为什么C ++ 0x中没有编译器生成的swap()方法?

时间:2010-01-16 18:44:23

标签: c++ language-design c++11

C ++编译器自动生成复制构造函数和复制赋值运算符。为什么不swap

目前,实现复制赋值运算符的首选方法是复制和交换习惯用法:

T& operator=(const T& other)
{
    T copy(other);
    swap(copy);
    return *this;
}

ignoring the copy-elision-friendly form that uses pass-by-value)。

这个习惯用法具有面对异常时的事务性优势(假设swap实现没有抛出)。相反,默认的编译器生成的复制赋值运算符递归地对所有基类和数据成员进行复制赋值,并且没有相同的异常安全保证。

同时,手动实施swap方法既繁琐又容易出错:

  1. 为了确保swap不抛出,必须为类和基类中的所有非POD成员,非POD成员等实现它。
  2. 如果维护者将新数据成员添加到类中,则维护者必须记住修改该类的swap方法。如果不这样做可能会引入微妙的错误。此外,由于swap是一种普通的方法,如果swap实现不完整,编译器(至少我不知道)不会发出警告。
  3. 如果编译器自动生成swap方法会不会更好?然后,隐式复制赋值实现可以利用它。

    显而易见的答案可能是:在开发C ++时,复制和交换习惯用法不存在,现在这样做可能会破坏现有代码。

    尽管如此,也许人们可以选择让编译器使用C ++ 0x用于控制其他隐式函数的相同语法生成swap

    void swap() = default;
    

    然后可能有规则:

    1. 如果存在编译器生成的swap方法,则可以使用copy-and-swap实现隐式复制赋值运算符。
    2. 如果没有编译器生成的swap方法,则会像以前一样实现隐式复制赋值运算符(在所有基类和所有成员上调用copy-assigment)。
    3. 有没有人知道是否已向C ++标准委员会提出这样的(疯狂?)事情,如果有,委员会成员有什么意见?

4 个答案:

答案 0 :(得分:23)

这是特里回答的补充。

我们必须在0x之前在C ++中创建swap函数的原因是因为一般的自由函数std::swap效率低(并且通用性较低)。它制作了一个参数的副本,然后进行了两次重新分配,然后释放了本质上浪费的副本。制作一个重量级的副本是浪费时间,当我们作为程序员知道我们真正需要做的就是交换内部指针和诸如此类的东西。

然而,rvalue-references完全缓解了这一点。在C ++ 0x中,swap实现为:

template <typename T>
void swap(T& x, T& y)
{
    T temp(std::move(x));
    x = std::move(y);
    y = std::move(temp);
}

这更有意义。我们只是在移动数据,而不是复制数据。这甚至允许交换不可复制的类型,如流。 C ++ 0x标准的草案规定,为了使类型与std::swap交换,它们必须是rvalue可构造的,并且rvalue可分配(显然)。

此版本的swap基本上可以执行任何自定义编写的交换功能。考虑我们通常为swap编写的一个类(例如这个“哑”向量):

struct dumb_vector
{
    int* pi; // lots of allocated ints

    // constructors, copy-constructors, move-constructors
    // copy-assignment, move-assignment
};

以前,swap会为我们的所有数据制作一份冗余副本,然后再将其丢弃。我们的自定义swap函数只会交换指针,但在某些情况下可能会使用笨拙。在C ++ 0x中,移动实现了相同的最终结果。调用std::swap会生成:

dumb_vector temp(std::move(x));
x = std::move(y);
y = std::move(temp);

转换为:

dumb_vector temp;
temp.pi = x.pi; x.pi = 0; // temp(std::move(x));
x.pi = y.pi; y.pi = 0; // x = std::move(y);
y.pi = temp.pi; temp.pi = 0; // y = std::move(temp);

编译器当然会摆脱冗余的赋值,留下:

int* temp = x.pi;
x.pi = y.pi;
y.pi = temp;

完全我们的自定义swap首先会做出什么。因此,在C ++ 0x之前,我同意你的建议,自定义swap不再是必需的,引入了rvalue-references。 std::swap将在任何实现移动函数的类中完美地工作。

事实上,我认为实施swap功能应该成为不好的做法。任何需要swap函数的类都需要rvalue函数。但在这种情况下,根本不需要自定义swap的混乱。代码大小确实增加(两个ravlue函数与一个swap),但rvalue-references不仅适用于交换,而是让我们有一个积极的权衡。 (整体代码更快,界面更清晰,代码更多,不再有swap ADL麻烦。)

至于我们是否可以default rvalue函数,我不知道。我会稍后再查看,或者其他人也可以查看,但这肯定会有所帮助。 :)

即便如此,允许default rvalue函数而不是swap是有意义的。所以从本质上讲,只要它们允许= default rvalue函数,您的请求就已经完成了。 :)

编辑:我做了一些搜索,= default移动的提案是提案n2583。根据{{​​3}}(我不知道如何阅读得很好),它被“移回”。它列在标题为“未准备好C ++ 0x,但将来可以重新提交”的部分中。所以看起来它不会是C ++ 0x的一部分,但可能会在以后添加。

有点令人失望。 :(

编辑2:再看一下,我发现了这个:this这是更近期的,看起来我们可以默认move。耶!

答案 1 :(得分:11)

交换,当由STL算法使用时,是一个自由函数。有一个默认的交换实现:std::swap。它显而易见。您似乎认为,如果您向数据类型添加交换成员函数,STL容器和算法将找到并使用它。事实并非如此。

如果你能做得更好,你应该专门化std :: swap(在你的UDT旁边的命名空间中,所以它是由ADL找到的)。将它推迟到成员交换功能是不恰当的。

虽然我们讨论这个主题,但它在C ++ 0x中也是惯用的(尽可能在这样一个新标准上有成语)将rvalue构造函数实现为交换。

是的,在一个成员交换是语言设计而不是自由函数交换的世界中,这意味着我们需要一个交换运算符而不是函数 - 或者原始类型(int,float等) )一般不能处理(因为它们没有成员函数交换)。那他们为什么不这样做呢?你必须要求委员会成员肯定 - 但我95%肯定原因是委员会长期以来首选库实现功能,而不是发明新语法来实现功能。交换运算符的语法很奇怪,因为不像=,+, - 等,以及所有其他运算符,每个人都不熟悉“交换”的代数运算符。

C ++在语法上足够复杂。他们竭尽全力不尽可能地添加新的关键字或语法功能,并且只是出于非常好的理由(lambdas!)。

答案 2 :(得分:2)

  

有没有人知道是否已向C ++标准委员会提出这样的(疯狂?)事情

发送电子邮件给Bjarne。他知道所有这些东西,通常会在几个小时内回复。

答案 3 :(得分:1)

甚至是编译器生成的移动构造函数/赋值计划(使用默认关键字)?

  

如果存在编译器生成的交换   方法,隐式复制赋值   运算符可以使用   复制和交换。

即使成语在异常的情况下使对象保持不变,这个成语不是通过要求创建第三个对象,而是首先使失败的机会更大吗?

可能还会有性能影响(复制可能比“分配”更昂贵),这就是为什么我没有看到编译器要实现这么复杂的功能。

一般情况下我不会重载operator =而且我不担心这种异常安全级别:我不会将个别分配包装到try块中 - 我最有可能对std::bad_alloc做什么点? - 所以我不在乎这个物体在最终被摧毁之前是否仍处于原始状态。当然可能会有一些特定的情况,你可能确实需要它,但我不明白为什么应该放弃“你不为你不使用的东西买单”的原则。