17

我指的是这个问题: 什么是复制和交换成语?

实际上,上述答案导致以下实现:

class MyClass
{
public:
    friend void swap(MyClass & lhs, MyClass & rhs) noexcept;

    MyClass() { /* to implement */ };
    virtual ~MyClass() { /* to implement */ };
    MyClass(const MyClass & rhs) { /* to implement */ }
    MyClass(MyClass && rhs) : MyClass() { swap(*this, rhs); }
    MyClass & operator=(MyClass rhs) { swap(*this, rhs); return *this; }
};

void swap( MyClass & lhs, MyClass & rhs )
{
    using std::swap;
    /* to implement */
    //swap(rhs.x, lhs.x);
}

但是,请注意,我们可以完全避开 swap(),执行以下操作:

class MyClass
{
public:
    MyClass() { /* to implement */ };
    virtual ~MyClass() { /* to implement */ };
    MyClass(const MyClass & rhs) { /* to implement */ }
    MyClass(MyClass && rhs) : MyClass() { *this = std::forward<MyClass>(rhs);   }
    MyClass & operator=(MyClass rhs)
    { 
        /* put swap code here */ 
        using std::swap;
        /* to implement */
        //swap(rhs.x, lhs.x);
        // :::
        return *this;
    }
};

请注意,这意味着我们将不再使用 MyClass 在 std::swap 上进行有效的参数相关查找。

简而言之,使用 swap() 方法有什么好处。


编辑:

我意识到在上面的第二个实现中存在一个可怕的错误,这是一个很大的事情,所以我将保持原样以指导遇到这个问题的任何人。

如果运算符 = 定义为

MyClass2 & operator=(MyClass2 rhs)

然后,只要 rhs 是一个 r 值,就会调用移动构造函数。但是,这意味着在使用时:

MyClass2(MyClass2 && rhs)
{
    //*this = std::move(rhs);
}

请注意,您最终会递归调用移动构造函数,因为 operator= 调用移动构造函数......

在您遇到运行时堆栈溢出之前,这是非常微妙且难以发现的。

现在解决这个问题的方法是同时拥有

MyClass2 & operator=(const MyClass2 &rhs)
MyClass2 & operator=(MyClass2 && rhs)

这允许我们将复制构造函数定义为

MyClass2(const MyClass2 & rhs)
{
    operator=( rhs );
}

MyClass2(MyClass2 && rhs)
{
    operator=( std::move(rhs) );
}

请注意,您编写了相同数量的代码,复制构造函数是“免费的”,您只需编写 operator=(&) 而不是复制构造函数和 operator=(&&) 而不是 swap() 方法。

4

4 回答 4

22

首先,无论如何你都做错了。复制和交换习语用于为赋值运算符重用构造函数(而不是相反),从已经正确构造的构造函数代码中受益,并为赋值运算符保证强大的异常安全性。但是您不要在移动构造函数中调用交换。与复制构造函数复制所有数据的方式相同(无论这在单个类的给定上下文中意味着什么),移动构造函数移动此数据,移动构造函数构造并分配/交换:

MyClass(const MyClass & rhs) : x(rhs.x) {}
MyClass(MyClass && rhs) : x(std::move(rhs.x)) {}
MyClass & operator=(MyClass rhs) { swap(*this, rhs); return *this; }

这将在您的替代版本中

MyClass(const MyClass & rhs) : x(rhs.x) {}
MyClass(MyClass && rhs) : x(std::move(rhs.x)) {}
MyClass & operator=(MyClass rhs) { using std::swap; swap(x, rhs.x); return *this; }

这不会表现出通过在构造函数中调用赋值运算符引入的严重错误。您永远不应该调用赋值运算符或在构造函数中交换整个对象。构造函数负责构建,并且具有不必关心先前数据的破坏的优势,因为该数据尚不存在。同样,构造函数是否可以处理不可默认构造的类型,最后但并非最不重要的是,直接构造比默认构造后跟赋值/交换的性能更高。

但是要回答您的问题,整个事情仍然是复制和交换的习惯用法,只是没有明确的swap功能。在 C++11 中它甚至更有用,因为现在您已经使用单个函数实现了复制和移动赋值。

如果交换函数在赋值运算符之外仍然有价值是一个完全不同的问题,并且取决于这种类型是否可能被交换,无论如何。事实上,在 C++11 中,具有正确移动语义的类型可以使用默认std::swap实现进行足够高效的交换,通常无需额外的自定义交换。请确保不要std::swap在赋值运算符中调用此默认值,因为它本身会进行移动赋值(这将导致与移动构造函数的错误实现相同的问题)。

但是再说一遍,不管是否自定义swap函数,这并没有改变复制和交换习语的用处,这在 C++11 中更有用,消除了实现额外函数的需要。

于 2012-10-17T10:48:56.730 回答
2

你当然没有考虑全貌。您的代码为不同的赋值运算符重用构造函数,原始代码为不同的构造函数重用赋值运算符。这基本上是一样的,你所做的只是改变它。

除了因为他们编写构造函数,他们可以处理非默认构造类型或如果没有像默认构造一样显式初始化int或对于默认构造来说很昂贵或者默认构造的成员对破坏无效(对于例如,考虑一个智能指针——未初始化的 T* 导致错误删除)。

所以基本上,你所取得的一切都是相同的原则,但在一个明显更糟的地方。哦,你必须定义所有四个函数,否则相互递归,而原始的复制和交换只定义了三个函数。

于 2012-10-17T10:08:09.850 回答
1

使用复制和交换习语来实现复制分配的原因(如果有的话)的有效性在 C++11 中与在以前的版本中相同。

另请注意,您应该std::move在移动构造函数中使用成员变量,并且应该std::move在作为函数参数的任何右值引用上使用。

std::forward应该只用于表单的模板参数引用T&&auto&&变量(在类型推导期间它们都可能被引用折叠成左值引用),以酌情保留它们的右值或左值。

于 2012-10-17T10:11:50.820 回答
-1

对于我来说最好在 C++11 中这样做:

class MyClass {
public:
    MyClass(MyString&& pStr) : str(pStr)) { 
        // call MyString(MyString&& pStr)
    }
    MyClass(const MyString& pStr) : str(pStr)) { 
        // call MyString(const MyString& pStr)
    }
private:
    MyString const str; // Still const!
};
于 2016-11-25T00:20:46.707 回答