复制和交换

如果你正在编写一个管理资源的类,则需要实现所有特殊成员函数(请参阅 Rule of Three / Five / Zero )。编写复制构造函数和赋值运算符的最直接方法是:

person(const person &other)
    : name(new char[std::strlen(other.name) + 1])
    , age(other.age)
{
    std::strcpy(name, other.name);
}

person& operator=(person const& rhs) {
    if (this != &other) {
        delete [] name;
        name = new char[std::strlen(other.name) + 1];
        std::strcpy(name, other.name);
        age = other.age;
    }

    return *this;
}

但这种方法存在一些问题。它没有强大的异常保证 - 如果 new[] 抛出,我们已经清除了 this 所拥有的资源并且无法恢复。我们在复制赋值中复制了许多复制构造的逻辑。我们必须记住自我分配检查,这通常只会增加复制操作的开销,但仍然很关键。

为了满足强大的异常保证并避免代码重复(使用后续的移动赋值运算符加倍),我们可以使用复制和交换习惯用法:

class person {
    char* name;
    int age;
public:
    /* all the other functions ... */

    friend void swap(person& lhs, person& rhs) {
        using std::swap; // enable ADL

        swap(lhs.name, rhs.name);
        swap(lhs.age, rhs.age);
    }

    person& operator=(person rhs) {
        swap(*this, rhs);
        return *this;
    }
};

为什么这样做?考虑一下我们遇到的情况

person p1 = ...;
person p2 = ...;
p1 = p2;

首先,我们从 p2 复制构造 rhs(我们不必在这里复制)。如果该操作抛出,我们在 operator= 中没有做任何事情,并且 p1 保持不变。接下来,我们在*thisrhs 之间交换成员,然后 rhs 超出范围。当 operator=,它隐含地清除了 this 的原始资源(通过析构函数,我们没有必要复制)。自我赋值也有效 - 复制和交换效率较低(涉及额外的分配和释放),但如果这是不太可能的情况,我们不会减慢典型用例来解释它。

Version >= C++ 11

上述公式原样用于移动分配。

p1 = std::move(p2);

在这里,我们从 p2 移动构造 rhs,其余的一切都是有效的。如果一个类是可移动但不可复制的,则不需要删除复制赋值,因为由于删除了复制构造函数,该赋值操作符将只是格式错误。