如何处理非平凡析构函数类型的可变参数模板构造函数

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

提问人:DWil 提问时间:10/10/2023 最后编辑:DWil 更新时间:10/11/2023 访问量:87

问:

我正在编写一个容器,该容器的行为类似于 std::optional,但如果发生错误,则保留 ErrorType。

我在转发具有非平凡析构函数的类型的构造时遇到了问题 - 特别是可以用 .std::initializer_list

我在下面写了一个例子,并在其中列出了一些问题,我希望这些问题能解释我正在挣扎的问题。int main()

您可以看到,由于非平凡的析构函数,无法构造 a。Result<MyStructList>

#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;

我正在 godbolt.org 上编译它x86_64 clang 17.0.1 和 -std=c++20 作为唯一的标志。

C++ 构造函数 variadic-templates

评论

1赞 Jarod42 10/10/2023
Result<MyStruct> object_9 = object_8;不是赋值,而是复制的构造函数。
1赞 NathanOliver 10/10/2023
Result<MyStruct> object_10 = std::move(object_9); 是移动构造,而不是分配。 始终是初始化,而不是赋值。T name = value
0赞 DWil 10/10/2023
答案是肯定的!傻傻的我。我现在已经解决了这个问题。谢谢两位。

答:

2赞 Jarod42 10/11/2023 #1

构造函数 #5 “包括”构造函数 #3 和构造函数 #4。

因此,删除构造函数 3 和构造函数 4。这修复了 .object_5

使用了非平凡的析构函数,这里有什么问题?

您应该提供析构函数来处理非平凡的析构函数:

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

演示

评论

0赞 DWil 10/11/2023
谢谢你@Jarod42。这正是我想要的。几个后续问题。删除更显式的构造函数 #3 和 #4 是否有任何问题。我没有意识到非平凡的析构函数的答案如此简单!这解决了我所有的疑问。总的来说,对这个容器有什么特别的评论吗?我是否遗漏了任何明显或不安全的东西?
0赞 Jarod42 10/11/2023
您也可以删除非常量复制分配。