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.
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();
}
}