c++constructorunioncopy-constructormove-constructor

How to write copy/move constructors with delegated constructors and conditional initialiser lists


I'm close to finishing my container but my last problem to solve is how to handle the copy/move constructor and appropriately construct the correct member variable inside the private union member variable. I want my copy and move constructor to only construct the appropriate type in the union depending on a bool error_state_ so that the types don't have to have a default constructor.

The container code is below including the concepts I use. If you check the copy/move constructor you can see what I'm attempting to do with the private tagged constructors.


template<typename ResultType, typename ErrorType>
concept move_assignable = std::is_move_assignable_v<ResultType> && std::is_move_assignable_v<ErrorType>;

template<typename ResultType, typename ErrorType>
concept copy_assignable = std::is_copy_assignable_v<ResultType> && std::is_copy_assignable_v<ErrorType>;

template<typename ResultType, typename ErrorType>
concept copy_constructible = std::is_copy_constructible_v<ResultType> && std::is_move_constructible_v<ErrorType>;

template<typename ResultType, typename ErrorType>
concept move_constructible = std::is_move_constructible_v<ResultType> && std::is_move_constructible_v<ErrorType>;

// This concept aims to avoid the variadic template constructors being picked
// over copy and move constructors.
template<typename ResultType, typename... Args>
concept exclude_result_type = (sizeof...(Args) != 1 && std::constructible_from<ResultType, Args...>)
                              || (sizeof...(Args) == 1 && std::constructible_from<ResultType, Args...>
                                  && !std::is_same_v<ResultType, std::decay_t<Args>...>);

template<typename ErrorType, typename... Args>
concept exclude_error_type = (sizeof...(Args) != 1 && std::constructible_from<ErrorType, Args...>)
                             || (sizeof...(Args) == 1 && std::constructible_from<ErrorType, Args...>
                                 && !std::is_same_v<ErrorType, std::decay_t<Args>...>);

// A Result class similar to Optional that can return ResultType or an
// ErrorType.
template<typename ResultType, typename ErrorType = Error> class Result
{
public:
  // ----------------------------------------
  // !! Construct a Result based on the ErrorType

  // R-value reference construction
  constexpr explicit Result(ErrorType&& error) : error_{ std::move(error) }, error_state_(true) {}

  // L-value reference construction
  constexpr explicit Result(const ErrorType& error) : error_{ error }, error_state_(true) {}
  // ----------------------------------------

  // ----------------------------------------
  // !! Construct a valid Result based on the ResultType

  // R-value reference construction
  constexpr explicit Result(ResultType&& result) : result_{ std::move(result) } {}

  // L-value reference construction
  constexpr explicit Result(const ResultType& result) : result_{ result } {}
  // ----------------------------------------

  // ----------------------------------------
  // Variadic template constructors
  template<typename... Args>
  constexpr Result(Args&&... args)
    requires exclude_result_type<ResultType, Args...>
    : result_{ std::forward<Args>(args)... }
  {}

  template<typename... Args>
  constexpr Result(Args&&... args)
    requires exclude_error_type<ErrorType, Args...>
    : error_{ std::forward<Args>(args)... }
  {}

  template<typename Type, typename... Args>
  constexpr Result(std::initializer_list<Type> list, Args&&... args)
    requires exclude_result_type<ResultType, Args...>
    : result_{ list, std::forward<Args>(args)... }
  {}

  template<typename Type, typename... Args>
  constexpr Result(std::initializer_list<Type> list, Args&&... args)
    requires exclude_error_type<ErrorType, Args...>
    : error_{ list, std::forward<Args>(args)... }
  {}
  // ----------------------------------------

  // ----------------------------------------
  // Copy constructor
  constexpr Result(const Result& result) noexcept
    requires copy_constructible<ResultType, ErrorType>
    : Result(result.error_state_ ? Result{ Tag<ErrorType>(), result } : Result{ Tag<ResultType>(), result })
  {
    // if (error_state_) {
    //   error_ = result.error_;
    // } else {
    //   result_ = result.result_;
    // }
  }
  // ----------------------------------------

  // ----------------------------------------
  // Move constructor
  constexpr Result(Result&& result) noexcept
    requires move_constructible<ResultType, ErrorType>
    : Result(result.error_state_ ? Result{ Tag<ErrorType>{}, std::move(result) }
                                 : Result{ Tag<ResultType>{}, std::move(result) })
  // : error_state_(result.error_state_)
  {
    // if (error_state_) {
    //   error_ = std::move(result.error_);
    // } else {
    //   result_ = std::move(result.result_);
    // }
  }
  // ----------------------------------------

  // ----------------------------------------
  // Destructor
  ~Result()
  {
    if (error_state_) {
      // cppcheck-suppress ignoredReturnValue
      error_.~ErrorType();
    } else {
      // cppcheck-suppress ignoredReturnValue
      result_.~ResultType();
    }
  }
  // ----------------------------------------

  // ----------------------------------------
  // Assignment operators
  // Copy assignment
  Result& operator=(const Result& result)
  {
    error_state_ = result.error_state_;
    if (error_state_) {
      error_ = result.error_;
    } else {
      result_ = result.result_;
    }
    return *this;
  }

  // Move assignment
  Result& operator=(Result&& result)
  {
    error_state_ = result.error_state_;
    if (error_state_) {
      error_ = std::move(result.error_);
    } else {
      result_ = std::move(result.result_);
    }
    return *this;
  }

  constexpr bool operator==(const Result<ResultType, ErrorType>& rhs) const
  {
    if (rhs.error_state_ == error_state_) {
      if (error_state_) {
        return rhs.error_ == error_;
      } else {
        return rhs.result_ == result_;
      }
    } else {
      return false;
    }
  }

  constexpr explicit operator bool() const noexcept { return !error_state_; }

  // Operator overload for dereferencing.
  // Not safe to do unless calling Good() before hand.
  [[nodiscard]] constexpr ResultType& operator*() noexcept
  {
    assertm(!error_state_, "Attempting to retrieve value when Result is in error!");
    return result_;
  }
  // Operator overload for dereferencing.
  // Not safe to do unless calling Good() before hand.
  [[nodiscard]] constexpr ResultType* operator->() noexcept
  {
    assertm(!error_state_, "Attempting to retrieve value when Result is in error!");
    return &result_;
  }

  [[nodiscard]] constexpr const ResultType& Value() const noexcept
  {
    assertm(!error_state_, "Attempting to retrieve value when Result is in error!");
    return result_;
  }

  [[nodiscard]] constexpr bool Good() const noexcept { return !error_state_; }

  [[nodiscard]] constexpr bool Bad() const noexcept { return error_state_; }

  // Not safe to call unless Bad() previously called.
  [[nodiscard]] constexpr const ErrorType& Error() const noexcept { return error_; }

private:
  template<typename T> struct Tag
  {
  };
  constexpr Result([[maybe_unused]] Tag<ErrorType> tag, const Result& result) noexcept
    : error_{ result.error_ }, error_state_(true)
  {}
  constexpr Result([[maybe_unused]] Tag<ResultType> tag, const Result& result) noexcept : result_{ result.result_ } {}
  constexpr Result([[maybe_unused]] Tag<ErrorType> tag, Result&& result) noexcept
    :  error_{ std::move(result.error_) }, error_state_(true)
  {}
  constexpr Result([[maybe_unused]] Tag<ResultType> tag, Result&& result) noexcept
    : result_{ std::move(result.result_) }
  {}

  union {
    ResultType result_;
    ErrorType error_;
  };
  bool error_state_{ false };
};

If I remove the delegated constructors and just use the commented move/copy assignment in those constructors everything works fine but doesn't use the appropriate constructor of the templated type.

Godbolt link to a demo: https://godbolt.org/z/xvvx4zT4s


Solution

  • I fixed this by removing the tagged delegating constructors and using placement new constructors instead.

      // ----------------------------------------
      // Copy constructor
      constexpr Result(const Result& result) noexcept
        requires copy_constructible<ResultType, ErrorType>
        : error_state_{ result.error_state_ }
      {
        if (error_state_) {
          new (&error_) ErrorType(result.error_);
        } else {
          new (&result_) ResultType(result.result_);
        }
      }
      // ----------------------------------------
    
      // ----------------------------------------
      // Move constructor
      constexpr Result(Result&& result) noexcept
        requires move_constructible<ResultType, ErrorType>
        : error_state_{ result.error_state_ }
      {
        if (error_state_) {
          new (&error_) ErrorType(std::move(result.error_));
        } else {
          new (&result_) ResultType(std::move(result.result_));
        }
      }
      // ----------------------------------------