根据构造函数实现 C++ 赋值运算符

Implement C++ assignment operator in terms of constructor

提问人:Daniel H 提问时间:12/1/2021 更新时间:12/1/2021 访问量:383

问:

背景

假设您要在 C++ 中实现资源管理类。不能使用规则或五默认值规则,因此实际上需要实现复制和移动构造函数、复制和移动赋值运算符和析构函数。在这个问题中,我将以 Box 为例,但这可以适用于许多不同的类型。

// Store object on heap but keep value semantics
// Moved-from state has null elem; otherwise should have value
// Only valid operations on moved-from state are assignment and destruction
// Assignment only offers a basic exception guarantee
template <typename T>
class Box {
public:
    using value_type = T;

    // Default constructor
    Box() : elem{new value_type} {}
    // Accessor
    value_type& get() { return *elem; }
    value_type const& get() const { return *elem; }

    // Rule of Five
    Box(Box const& other) : elem{new value_type{*(other.elem)}} {};
    Box(Box&& other) : elem{other.elem}
    {
        other.elem = nullptr;
    };
    Box& operator=(Box const& other)
    {
        if (elem) {
            *elem = *(other.elem);
        } else {
            elem = new value_type{*(other.elem)};
        }
        return *this;
    }
    Box& operator=(Box&& other)
    {
        delete elem;
        elem = other.elem;
        other.elem = nullptr;
        return *this;
    }
    ~Box()
    {
        delete elem;
    }

    // Swap
    friend void swap(Box& lhs, Box& rhs)
    {
        using std::swap;
        swap(lhs.elem, rhs.elem);
    }
private:
    T* elem;
};

(请注意,更好的实现将具有更多的功能,例如函数,基于构造函数的转发构造函数,分配器支持等;在这里,我只实现问题和测试所需的内容。它也可能使用 保存一些代码,但这会使它成为一个不太清楚的例子)Boxnoexceptconstexprexplicitvalue_typestd::unique_ptr

请注意,赋值运算符彼此之间、与各自的构造函数以及与析构函数共享大量代码。如果我不想允许将赋值从 en 移出,这种情况会有所减少,但在更复杂的类中会更加明显。Box

题外话:复制和交换

处理此问题的一种标准方法是使用复制和交换习语(在这种情况下也称为四条半规则),如果满足以下条件,它也会为您提供强大的例外保证:swapnothrow

    // Copy and Swap (Rule of Four and a Half)
    Box& operator=(Box other)  // Take `other` by value
    {
        swap(*this, other);
        return *this;
    }

这允许您只编写一个赋值运算符(如果可能,按值获取可以让编译器将另一个赋值运算符移动到参数中,并在必要时为您执行复制),并且该赋值运算符很简单(假设您已经有)。但是,正如链接文章所说,这存在一些问题,例如在操作过程中进行额外分配和保留内容的额外副本。otherswap

理念

我以前从未见过的是我称之为 Destroy-and-Initialize 赋值运算符的东西。既然我们已经在构造函数中完成了所有工作,并且对象的赋值版本应该与复制构造的版本相同,为什么不使用构造函数,如下所示:

    // Destroy-and-Initialize Assignment Operator
    template <typename Other>
    Box& operator=(Other&& other)
    requires (std::is_same_v<Box, std::remove_cvref_t<Other>>)
    {
        this->~Box();
        new (this) Box{std::forward<Other>(other)};
        return *std::launder(this);
    }

这仍然像 Copy-and-Swap 一样执行额外的分配,但仅在复制分配情况下而不是在移动分配情况下进行,并且在销毁 的一个副本执行此操作,因此它不会在资源限制下失败。T

问题

  1. 以前是否提出过这个问题,如果有,我在哪里可以阅读更多关于它的信息?
  2. 在某些情况下,如果 是其他事物的子对象,或者是否允许销毁和重新构造子对象,则此 UB 是否有效?Box
  3. 这有什么我没有提到的缺点,比如不兼容吗?constexpr
  4. 有没有其他选项可以避免赋值运算符代码的重用,比如这个和四分半法则,当你不能只使用它们时?= default
C++ 复制和交换 零法 则五

评论

2赞 NathanOliver 12/1/2021
如果类的任何非静态成员是限定的或引用类型,则此方法将具有 UB:stackoverflow.com/a/58415092/4342498const
0赞 Daniel H 12/1/2021
@NathanOliver是的,但其他任何方法不起作用,除非您想对该成员的处理进行特殊情况,并且在分配时不更改它是有意义的。
0赞 Phil1970 12/1/2021
如果复制构造函数抛出,这种方法将导致无效对象。
0赞 Daniel H 12/1/2021
@Phil1970啊,你是对的,至少在我的例子中,如果不指定它需要 nothrow copy constructable,就没有办法避免它,否则不需要。即便如此,由于复制构造函数正在分配内存,这也可能失败。在这种情况下,它可以用于移动分配,从而避免了一些代码重用,但不会那么多。BoxT
0赞 Phil1970 12/2/2021
我认为复制和交换在 99% 的时间内就足够了。额外的副本(如果需要时)使分配具有事务性,这通常是一件好事。如果确实需要更多控制,可以随时自己编写运算符。

答: 暂无答案