C++11中,复制并交换惯用法仍然有用吗?

21

我参考了这个问题:什么是拷贝交换惯用语?

实际上,上面的答案引导出以下实现:

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是一个右值,移动构造函数就会被调用。但是这意味着在使用以下语句时:

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()方法。


5
仍然是复制并交换,没有针对"MyClass"的特定“swap”函数。在使用*this = std::forward<MyClass>(rhs); /* that should be 'move', btw */代替swap(*this, rhs);有什么好处?你只是延迟了调用。 - Xeo
2
你在这里使用的forward是不正确的,因为它被不必要地使用了(它应该在模板函数内使用,其中std::forward<T>中的T实际上可以更改;而在这里它总是MyClass)。只需使用std::move即可。是的,这似乎更加简洁,我个人喜欢将赋值和交换分开(后者恰好使用前者)。 - GManNickG
我会注意正确使用forward。我在考虑如果一些STL函数实际上会在您的代码中调用swap(),那么拥有swap函数可能更好。 - aCuria
@Xeo 第二个版本更简单,对于不熟悉交换习惯语/咒语/咒语的任何人来说更容易理解。所有事情都相等的话,我更喜欢更简洁的方法。 - aCuria
3个回答

25

首先,无论如何你都做错了。拷贝并交换惯用法的存在是为了重复使用构造函数以进行赋值运算符(而不是相反),从已经正确构造构造函数代码中获益,并保证赋值运算符具有强异常安全性。但在移动构造函数中你没有调用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; }

这种方法不会在构造函数内部调用赋值运算符引入严重的错误。你永远不应该在构造函数内部调用赋值运算符或交换整个对象。构造函数存在的目的是负责构造,由于先前的数据不存在,所以它们有不必关心上一个数据的销毁的优点。同样,构造函数可以处理不能默认构造的类型,最后但是最不重要的是直接构造可能比默认构造后进行赋值/交换更高效。

但是回答你的问题,整个过程仍然是复制并交换惯用语,只是没有显式的swap函数。在C++11中,这更加有用,因为现在你已经使用单个函数实现了复制移动分配。

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

但是再次说一遍,无论是否具有自定义swap函数,这都不会改变复制并交换惯用语的实用性,在C++11中更为实用,消除了实现额外函数的需要。


+1 但这引出了一个问题:为什么不在移动构造函数中使用swap()呢?在这种情况下,swap()所做的只是交换(x, rhs.x),这与您在构造函数中所做的相同。 - aCuria
@aCuria 因为移动构造函数会进行移动构造,所以默认构造然后交换/赋值在概念上是错误的。为什么要与实际上还不存在的东西交换呢?在最好的情况下,通常也是性能上的错误(除了概念上的错误),而在最坏的情况下,如果成为惯例,它可能会导致类似于分配错误的错误。嗯,最好的情况实际上是那些无法默认构造的类根本无法编译... - Christian Rau
1
“这与您在构造函数中所做的相同” - 不,它并不相同。我的构造函数是移动构造(对于各个成员类型意味着什么)。移动不等同于交换。由于移入的值实际上不存在,它们当前正在被构造。因此,您无法将任何内容交换到 rhs.x 中。同样,与 MyClass(const MyClass &rhs) { x = rhs.x; } 相比,MyClass(const MyClass &rhs) : x(rhs.x) {} 在概念上是错误的,交换在构造函数中也是概念上错误的,无论是否导致类似的代码。 - Christian Rau
@aCuria 深入探讨赋值和初始化的区别,以理解这两个概念之间的差异。 - Christian Rau
不确定我完全理解了这个。您是在声称使用上面的代码片段时,不需要实现交换,而只需使用 std::swap 吗?std::swap 调用移动赋值操作,并且移动赋值操作调用 swap,因此如果没有显式定义 swap,则似乎会出现相互递归的情况。最好独立地定义移动赋值,然后使用 std::swap。然后再根据其他函数定义复制赋值。 - Nir Friedman
显示剩余3条评论

2
你肯定没有考虑全局。你的代码重用了不同赋值运算符的构造函数,原始代码重用了不同构造函数的赋值运算符。这本质上是相同的事情,你只是将其变换了一下。
除非他们编写构造函数,否则他们无法处理非默认可构造类型或者值在未显式初始化时出错的类型,例如 int,或者纯粹的昂贵默认构造函数或者默认构造成员无法有效拆卸(例如,考虑智能指针——未初始化的 T* 导致坏 delete)。
所以基本上,你所实现的原理是相同的,但位置更差。噢,而且你需要定义所有四个函数,否则存在互递归,而最初的复制和交换只定义了三个函数。

你关于声明函数数量的说法是错误的。它应该是"swap, assignment, cctor, mctor"与"assignment(&), assignment(&&), cctor, mctor",因此两种方式都有4个函数。 - aCuria
@aCuria 这只适用于您带有额外的swap方法的版本。但是,您可以不使用额外的swap函数来执行此操作,并仍然仅定义一个赋值运算符,即当未编写不正确的构造函数时,如我的答案所示。 - Christian Rau

1

在 C++11 中,使用复制并交换惯用语实现复制赋值的原因(如果有的话)与之前的版本相同。

另外,请注意,在移动构造函数中应对成员变量使用 std::move,并且在任何函数参数中引用右值引用时都应使用 std::move

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接