使用特殊的复制赋值运算符而不是简单的析构函数和就地构造函数的原因

Reasons to have special copy assignment operator instead of simple destructor and in-place constructor

提问人:Vasilii Rogin 提问时间:4/21/2023 更新时间:4/21/2023 访问量:131

问:

我有一个具有自己的资源管理的类:

class Lol
{

private:
  // This is data which this class allocates
  char *mName = nullptr;

public:
    Lol(std::string str) // In constructor just copy data from string
    {
        auto cstr = str.c_str();
        auto len = strlen(cstr);
        mName = new char[len + 1];
        strcpy(mName, cstr);
    };

    ~Lol() // And of course clean up
    {
        delete[] mName;
    }
}

我实现了复制构造函数,它只是复制托管数据:

    Lol(const Lol &other)
    {
        auto cstr = other.mName;
        auto len = strlen(cstr);
        mName = new char[len + 1];
        strcpy(mName, cstr);
    };

我还需要实现复制分配运算符。我刚刚这样做了:

    Lol &operator=(const Lol &other)
    {
        if (this == &other)
        {
            return *this;
        }

        // Clean up my resources
        this->~Lol();
 
        // And copy resources from "other" using already implemented copy constructor
        new (this) Lol(other);
    }

看起来这个复制分配运算符将适用于所有类。为什么我需要在复制赋值运算符中包含另一个代码?它的用例是什么?

C++ 三法 五法则

评论

1赞 NathanOliver 4/21/2023
如果您的类具有 const 或 reference 成员,则放置新解决方案不起作用。
0赞 Mooing Duck 4/21/2023
此外,如果构造函数引发异常,则应用将具有未定义的行为,并且无法修复它
0赞 PaulMcKenzie 4/21/2023
this->~Lol(); new (this) Lol(other);--我不得不问--有这么简单的一堂课,突然想到这样做的想法是从哪里来的?使用 placement-new 的原因是什么?为什么不简单地:{ Lol temp(other); std::swap(mName, temp.mName); return *this; }
1赞 Mooing Duck 4/21/2023
有了适当的 RAII,这个问题就消失了。 或者或者......std::unique_ptr<char[]>std::vector<char>std::string
0赞 Mooing Duck 4/21/2023
拥有移动构造函数和移动赋值也意味着复制赋值是微不足道的复制和交换。

答:

2赞 HolyBlackCat 4/21/2023 #1

如果构造函数抛出,则必须捕获异常并以某种方式恢复(通过调用一些构造函数,可能是默认的构造函数),这使得它变得不那么优雅。未能调用构造函数将导致双重销毁和 UB。

此外,如果您从此类继承,或将其用作成员变量,则此方法会导致 UB:[[no_unique_address]]

[basic.life]/8.4

如果满足以下条件,则对象 o1 可被对象 o2 透明地替换:...

— O1 和 O2 都不是潜在重叠的子对象......

[intro.object]/7

可能重叠的子对象是:

— 基类子对象,或者

— 使用 no_unique_address 属性声明的非静态数据成员。

这本身不是 UB,但如果你的对象不是透明可替换的,那么重建的对象必须在使用前进行编辑,这是不切实际的(例如,如果它是一个自动变量,自动销毁将在没有 UB 的情况下发生→)。std::launderstd::launder

C++17 有更多的限制。如果您的类包含 const 或 reference 成员,您还需要 (C++17 [basic.life]/8.3)。std::launder


如果您正在寻找通用赋值运算符,那么有一个。它被称为复制和交换习语。看:

MyClass &operator=(MyClass other) noexcept
{
    std::swap(x, other.x); // Swap every member here.
    return *this;
}

这既充当复制赋值,又充当移动赋值(如果您有相应的构造函数),并提供强异常保证(如果抛出复制,则目标对象保持不变)。

唯一一种情况(据我所知)它不能开箱即用是当类在某个地方(可能在自身内部)维护指向自己的指针时。

评论

0赞 Vasilii Rogin 4/21/2023
为什么有效?如果我有这个代码:这里我调用复制赋值运算符。我希望这里 lol1 和 lol2 都有不同的缓冲区,具有相同的数据。但是如果我交换,那么 lol2 将有“lol1”,不是吗?Lol lol1{"lol1"}; Lol lol2{"lol2"}; lol1 = lol2;
0赞 HolyBlackCat 4/21/2023
@VasiliiRogin 请注意,该参数是按值传递的。因此,首先复制(在本例中)或移动到参数中,然后将参数交换为 ,然后销毁参数(保留旧值 )。lol2lol1lol1
0赞 Red.Wave 4/21/2023
复制/交换习语是可复制资源管理类的第一个解决方案。它最常工作得很好。在某些情况下,您可以找到更好的优化替代方案;但是复制/交换习惯用语是一种模式,通常会导致功能正确的代码。
1赞 Martin York 4/21/2023 #2

使用 copy and swap 成语。

Lol &operator=(const Lol &other)
{
    if (this == &other)
    {
        return *this;
    }

    // Clean up my resources
    this->~Lol();

    // Copy can throw.
    // Then your object is in an undefined state.
    new (this) Lol(other);

    // You forgot the return:
    return *this;
}

因此,这并不能提供强有力的(或任何)例外保证。

首选方式是:

Lol& operator=(Lol const& other)
{
    Lol   copy(other);         // Here we use the copy constructor
                               // And the destructor at the end of
                               // function cleans up the scope
                               // Note this happens after the swap
                               // so you are cleaning up what was in
                               // this object.
    swap(copy);
    return *this;
}
void swap(Lol& other) noexcept
{
    std::swap(mName, other.mName);
}

如今,我们已经改进了这个原始版本

Lol& operator=(Lol copy)      // Notice we have moved the copy here.
{
    swap(copy);
    return *this;
}

令人兴奋的是,此版本的作业同样有效地适用于复制和移动作业。