复制和交换习惯用法在C ++ 11中是否仍然有用

时间:2012-10-16 19:32:30

标签: c++ c++11 swap

我指的是这个问题: What is the copy-and-swap idiom?

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

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()方法有任何好处。


编辑:

我意识到在上面的第二个实现中存在一个可怕的错误,这是一个非常重要的事情,所以我会留下它来指导任何碰到这个的人。

如果operator =定义为

MyClass2 & operator=(MyClass2 rhs)

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

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

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

在获得运行时堆栈溢出之前,这非常微妙且难以发现。

现在解决这个问题的方法是同时使用

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

这允许我们将副本构造函数定义为

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

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

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

4 个答案:

答案 0 :(得分:21)

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

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; }

通过在构造函数中调用赋值运算符,不会出现严重错误。您永远不应该调用赋值运算符或在构造函数中交换整个对象。构造者在那里关心构造,并且具有不必关心以前数据的破坏的优点,因为该数据尚不存在。同样,构造函数可以处理类型而不是默认构造和最后但是最不经常直接构造比defualt构造更高效,然后是赋值/交换。

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

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

但是再说一遍,自定义swap函数与否,这并没有改变复制和交换习惯用法的任何内容,这在C ++ 11中更有用,消除了需要实现一个额外的功能。

答案 1 :(得分:2)

你当然不会考虑整体情况。您的代码重用了不同赋值运算符的构造函数,原始的重用运算符用于不同的构造函数。这基本上是一回事,你所做的就是改变它。

除了因为它们编写构造函数之外,它们可以处理非默认构造类型或类型,如果没有像int那样显式地初始化,那么它们的值是坏的,或者对于默认构造或者默认构造的情况来说是非常昂贵的成员无法销毁(例如,考虑智能指针 - 未初始化的T *导致错误删除)。

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

答案 2 :(得分:1)

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

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

std::forward仅应用于T&&形式的模板参数引用以及auto&&变量(两者都可能在类型推导期间被引用折叠为左值引用)以保留适当的左值或左值。

答案 3 :(得分:0)

对于mebest在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!
};