7

通常,给定某种类型T,要实现复制和移动赋值,需要两个函数

T& operator=(T&&) { ... }
T& operator=(const T&) { ... }

最近,我开始意识到一个人就足够了

T& operator=(T v) {
  swap(v);
  return *this;
}

此版本利用了复制/移动构造函数。分配是复制还是移动取决于如何v构造。这个版本甚至可能比第一个版本更快,因为按值传递为编译器优化提供了更多空间 [1]。那么,第一个版本比第二个版本甚至标准库都使用它的优势是什么?

[1] 我想这解释了为什么标记和函数对象在标准库中按值传递。

4

2 回答 2

5

std::swap是通过执行移动构造和两个移动分配操作来实现的。因此,除非您实现自己的swap操作来替换标准提供的操作,否则您的代码将是一个无限循环。

因此,您可以实现两种operator=方法,也可以实现一种operator=方法和一种swap方法。就调用的函数数量而言,它最终是相同的。

此外,您的版本operator=有时效率较低。除非参数的构造被省略,否则构造将通过调用者值的复制/移动来完成。紧随其后的是 1 个移动构造和 2 个移动分配(或您swap所做的任何事情)。而适当的operator=重载可以直接使用给定的参考。

这假设您无法编写实际分配的最佳版本。考虑将一个复制分配vector给另一个。如果目标vector有足够的存储空间来容纳源向量的大小......你不需要分配。而如果您复制构造,则必须分配存储空间。只有这样才能释放您本来可以使用的存储空间。

即使在最好的情况下,您的复制/移动和交换也不会比使用值更有效。毕竟,您将引用参数;std::swap不适用于价值观。因此,无论您认为使用引用会失去什么效率,都将失去任何一种方式。

支持复制/移动和交换的主要论点是:

  1. 减少代码重复。仅当您的复制/移动分配操作的实现与复制/移动构造或多或少相同时,这才是有利的。许多类型并非如此。如前所述,vector可以通过尽可能使用现有存储来优化自身。事实上,许多容器都可以(尤其是序列容器)。

  2. 以最小的努力提供强大的异常保证。假设您的移动构造函数是 noexcept。

就个人而言,我更愿意完全避免这种情况。我更喜欢让编译器生成我所有的特殊成员函数。如果一个类型绝对需要我编写那些特殊的成员函数,那么这个类型将尽可能地少。也就是说,它的唯一目的是管理需要此操作的任何内容。

这样,我就不用担心了。我的大部分类不需要显式定义任何这些函数。

于 2016-01-15T05:08:27.867 回答
1

我意识到这是一个公认的答案,但我觉得我必须加入。这里有两个不同的问题:

  1. 统一赋值运算符。这意味着您有一个赋值运算符,它采用值,而不是两个重载,采用 const & 和 &&。
  2. 复制和交换 (CAS) 复制赋值运算符。

如果做1,一般做2。因为需要在某处实现swap/move赋值逻辑,而在统一赋值运算符中无法实现,所以一般实现swap并调用。但是做2并不意味着你必须做1:

T& operator=(T&&) { /* actually implemented */ }
T& operator=(const T& t) { T t2(t); swap(*this, t2); return *this;}

在这种情况下,我们实现了移动分配,但使用了默认的交换(它执行一个移动构造和两个移动分配)。

做 CAS 的动机是获得强大的异常保证,尽管正如 TC 在评论中指出的那样,你可以这样做:

T& operator=(const T& t) { *this = T(t); return *this;}

这可能更有效。在我编写的大多数代码中,性能是一个问题,我从来不需要强大的异常保证,所以我几乎永远不会这样做,所以它只取决于你的用例。

你应该永远不要做1。最好它们是单独的函数,以便可以将移动分配标记为 noexcept。

于 2016-01-15T06:15:32.030 回答