Search code examples
c++constructorvariadic-templates

How do I handle variadic template constructors for non-trivial destructor types


I'm writing a container that acts similarly to std::optional but holds an ErrorType if an error occurs.

I'm having issues with forwarding on construction of types that have non-trivial destructors - specifically things that can be initialized with std::initializer_list.

I've written an example below and laid out some questions in the int main() that I hope will explain what I'm struggling with.

You can see that it's not possible to construct a Result<MyStructList> because of the non-trivial destructor.

#include <array>
#include <initializer_list>
#include <iostream>
#include <type_traits>
#include <utility>
#include <vector>

struct MyStruct {
  MyStruct(int i, double d, const char *c) : i(i), d(d), c(c) {}
  int i = 0;
  double d = 10.0;
  const char *c = "hello";
};

// Prove MyStruct is trivially destructible
static_assert(std::is_trivially_destructible_v<MyStruct>);

struct MyStructList {
  std::vector<int> a;
};

// Prove MyStructList is NOT trivially destructible
static_assert(!std::is_trivially_destructible_v<MyStructList>);

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

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

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

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

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

  // ----------------------------------------
  // Variadic template constructor

  // Constructor 5 - Variadic template constructor to forward construction to
  // the ResultType
  template <typename... Args>
  constexpr Result(Args &&...args)
    requires std::constructible_from<ResultType, Args...>
      : result_(std::forward<Args>(args)...) {}

  // Constructor 6 - Variadic template constructor for initialiser list
  template <typename Type, typename... Args>
  constexpr Result(std::initializer_list<Type> list, Args &&...args)
    requires std::constructible_from<ResultType, Args...>
      : result_(list, std::forward<Args>(args)...) {}
  // ----------------------------------------

  // ----------------------------------------
  // Copy constructor
  // Constructor 7
  constexpr Result(const Result &result)
      : error_state_(result.error_state_){
        if(error_state_){
            error_ = result.error_;
        } else {
            result_ = result.result_;
        }
      }
  // ----------------------------------------

  // ----------------------------------------
  // Move constructor
  // Constructor 8
  // Probably cheaper to just copy the error_state_ bool than move it
  constexpr Result(Result &&result) : error_state_(result.error_state_) {
    if (error_state_) {
      error_ = std::move(result.error_);
    } else {
      result_ = std::move(result.result_);
    }
  }
  // ----------------------------------------

  // ----------------------------------------
  // 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;
  }

  // Non const copy assignment
  Result& operator=(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;
  }

  ResultType &operator()() { return result_; }

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

int main() {
  // Uses Constructor 1
  Result<MyStruct> object_1("Error"); // Compiles
  
  // Use Constructor 2
  const char *my_error = "Error";
  Result<MyStruct> object_2(my_error); // Compiles

  // Use Constructor 3
  const MyStruct my_struct(0, 1.0, "Test3");
  const Result<MyStruct> object_3(my_struct); // Compiles

  //// Use Constructor 4
  Result<MyStruct> object_4(MyStruct{0, 1.0, "Test4"}); // Compiles
  // Result<MyStruct> object_5({0, 1.0, "Test5"}); // Doesn't compile, Is there
  // a way to fix this? The compiler fails about ambiguity.

  // Use Constructor 5
  Result<MyStruct> object_6{0, 1.0, "Test6"}; // Compiles
  // Result<MyStruct> object_7{0, 1.0, 1.0}; // Doesn't compile, as expected due
  // to concept

  // Use Constructor 6
  // How to forward initialiser list construction. How to make this work?
  // Result<MyStructList> object_8{{0,1,2}}; // Doesn't compile. Non trivial destructor is used, what's the problem here?

  // Use Constructor 7
  Result<MyStruct> object_7(object_6); // Compiles
  std::cout << object_7().c << std::endl;

  // Use Constructor 8
  Result<MyStruct> object_8(std::move(object_6));
  std::cout << object_8().c << std::endl;

  // Use Const Copy assignment
  Result<MyStruct> object_9{0, 1.0, "Test9"};
  object_9 = object_3; 

  // Use Copy assignment
  Result<MyStruct> object_10{0, 1.0, "Test9"};
  object_10 = object_9; 

  // Use Move assignment
  Result<MyStruct> object_11{0, 1.0, "Test10"};
  object_11 = std::move(object_10);

  return 0;

I'm compiling this on godbolt.org with x86_64 clang 17.0.1 and -std=c++20 as the only flag.


Solution

  • Constructor #5 "includes" Constructor #3 and Constructor #4.

    So remove Constructor 3 and Constructor 4. That fixes ambiguity about object_5.

    Non trivial destructor is used, what's the problem here?

    You should provide destructor to handle non-trivial destructor:

    ~Result() {
        if (error_state_) {
            error_.~ErrorType();
        } else {
            result_.~ResultType();
        }
    }
    

    Demo