编写自动分配安全复制函数的正确方法以及为什么? C++

The correct way to write auto-assigment-safe copy functions and why? C++

我正在读一本关于 c++ 的书,在 "Copy Control" 部分作者教我们在 class 中编写 operator= 告诉我们必须确保该方法当我们有一个使用动态内存的 class 时,self-assigment 是安全的。

所以,假设我们有一个名为 "Bank_Client" 的 class,其中有一个 std::string 是用 new 创建的。这本书教我们这样做以避免自我赋值的情况:

Bank_Client& Bank_Client::operator=(const Bank_Client &addres){
    std::string *temp = new std::string(*addres.name);
    delete name;
    name = temp;
    return *this;
}

所以如果我这样做

Bank_Client bob("Bobinsky");
bob = bob;

该程序不会只是爆炸。但是就在我认为 temp 变量是浪费时间的时候,本书的作者向我们展示了另一种方法:

Bank_Client& Bank_Client::operator=(const Bank_Client &addres){
    if (this != &addres){
        delete name;
        name = new std::string(*addres.name);
    }
    return *this;
}

就像他读懂了我的想法一样。但紧接着他告诉我们永远不要那样做,最好用另一种方式来做,但从不解释原因。

为什么第一种方法更好?它更慢,不是吗? 最好的原因是什么?

assert检查没有自赋值怎么办? (因为我们真的不需要它)。然后用相应的NDEBUG去激活它,这样检查就不会浪费时间了。

第一种方式较慢如果一个对象正在被自我分配。但是,自赋值 很少见 。在所有其他情况下,the additional if check from the second approach is a waste.

也就是说,这两种 方法都不是实现复制赋值运算符的糟糕方法:两者都不是异常安全的。如果分配中途失败,您将留下处于某种不一致状态的分配一半的对象。同样糟糕的是,它部分地复制了复制构造函数的逻辑。相反,您应该使用 the copy-and-swap idiom.

实现复制赋值运算符

你应该使用复制和交换。为此,您需要一个复制构造函数(如果您还想使用移动语义,可能还需要一个移动构造函数)。

class Bank_Client {
    // Note that swap is a free function:
    // This is important to allow it to be used along with std::swp
    friend void swap(Bank_Client& c1, Bank_Client& c2) noexcept {
        using std::swap;
        swap(c1.name, c2.name);
        // ...
    }
    // ...
};

// Note that we took the argument by value, not const reference
Bank_Client& Bank_Client::operator=(Bank_Client address) {
    // Will call the swap function we defined above
    swap(*this, adress);
    return *this;
}

现在,让我们看一下客户端代码:

Bank_Client bob("Bobinsky");
// 1. Copy constructor is called to construct the `address` parameter of `operator=()`
// 2. We swap that newly created copy with the current content of bob
// 3. operator=() returns, the temporary copy is destroyed, everything is cleaned up!
bob = bob;

显然,你在进行自赋值时创建了一个无用的副本,但它的优点是允许你在你的复制构造函数(或移动构造函数)中重用逻辑并且它是异常安全的:如果初始副本什么都不做抛出异常。

另一个好处是您只需要 operator=() 的一个实现来处理复制和移动语义(复制和交换以及移动和交换)。如果性能是一个大问题,您仍然可以使用右值重载 operator=(Bank_Client&&) 来避免额外的移动(尽管我不鼓励这样做)。

最后,我还建议您尽量依赖 rule of 0,如上例所示,如果 class 的内容发生变化,您还必须相应地更新交换功能。