使用单个函数实现复制和移动赋值
Implementing copy and move assignment with a single function
通常,给定某种类型T
,要实现复制和移动赋值,需要两个函数
T& operator=(T&&) { ... }
T& operator=(const T&) { ... }
最近才发现,一个就够了
T& operator=(T v) {
swap(v);
return *this;
}
此版本利用了 copy/move 构造函数。赋值是复制还是移动取决于 v
的构造方式。这个版本甚至可能比第一个版本更快,因为按值传递允许更多 space 用于编译器优化 [1]。那么,第一个版本比第二个版本有什么优势,即使是标准库也使用它?
[1] 我猜这解释了为什么标签和函数对象在标准库中按值传递。
std::swap
是通过执行移动构造然后执行两次移动赋值操作来实现的。因此,除非您实现自己的 swap
操作来替换 standard-provided 操作,否则您提供的代码是一个无限循环。
因此您可以实现 2 个 operator=
方法,或者实现一个 operator=
方法和一个 swap
方法。就调用函数的数量而言,它最终是相同的。
此外,您的 operator=
版本有时效率较低。除非省略参数的构造,否则该构造将通过来自调用者值的 copy/move 完成。接下来是 1 个移动构造和 2 个移动分配(或您的 swap
所做的任何事情)。而适当的 operator=
重载可以直接使用给定的引用。
并且这是假设您无法编写实际作业的最佳版本。考虑 copy-assigning 一个 vector
到另一个。如果目标 vector
有足够的存储空间来容纳源向量的大小......你不需要分配。而如果您复制 construct,则必须分配存储空间。只有这样才能释放您本可以使用的存储空间。
即使在最好的情况下,您的 copy/move&swap 也不会比使用值 更有效。毕竟,您将引用参数; std::swap
不适用于值。因此,无论您认为使用引用会失去什么效率,都会以任何一种方式失去。
支持 copy/move&swap 的主要论点是:
减少代码重复。这只有在您的 copy/move 赋值操作的实现与 copy/move 构造或多或少相同时才有用。许多类型并非如此。如前所述,vector
可以通过尽可能使用现有存储来优化自身。事实上很多容器都可以(特别是序列容器)。
以最小的努力提供强大的异常保证。假设你的移动构造函数是 noexcept.
就个人而言,我更愿意完全避免这种情况。我更喜欢让编译器生成我所有的特殊成员函数。而如果一个类型绝对需要我写那些特殊的成员函数,那么这个类型将尽可能地精简。也就是说,它的唯一目的是管理需要此操作的任何内容。
这样我就不用担心了。我的大部分 类 不需要显式定义这些函数中的任何一个。
我知道这有一个公认的答案,但我觉得我必须跳进去。这里有两个不同的问题:
- 统一赋值运算符。这意味着您有一个按值获取的赋值运算符,而不是两个由 const & 和 && 获取的重载。
- 复制和交换 (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的动机是为了获得强异常保证,尽管T.C。在评论中指出,你可以这样做:
T& operator=(const T& t) { *this = T(t); return *this;}
这可能更多 efficient.In 我编写的大部分代码,性能是一个问题,我从来不需要强大的异常保证,所以我几乎永远不会这样做,所以这取决于你用例。
你永远不应该做 1。它们最好是单独的函数,这样移动赋值就可以标记为 noexcept。
通常,给定某种类型T
,要实现复制和移动赋值,需要两个函数
T& operator=(T&&) { ... }
T& operator=(const T&) { ... }
最近才发现,一个就够了
T& operator=(T v) {
swap(v);
return *this;
}
此版本利用了 copy/move 构造函数。赋值是复制还是移动取决于 v
的构造方式。这个版本甚至可能比第一个版本更快,因为按值传递允许更多 space 用于编译器优化 [1]。那么,第一个版本比第二个版本有什么优势,即使是标准库也使用它?
[1] 我猜这解释了为什么标签和函数对象在标准库中按值传递。
std::swap
是通过执行移动构造然后执行两次移动赋值操作来实现的。因此,除非您实现自己的 swap
操作来替换 standard-provided 操作,否则您提供的代码是一个无限循环。
因此您可以实现 2 个 operator=
方法,或者实现一个 operator=
方法和一个 swap
方法。就调用函数的数量而言,它最终是相同的。
此外,您的 operator=
版本有时效率较低。除非省略参数的构造,否则该构造将通过来自调用者值的 copy/move 完成。接下来是 1 个移动构造和 2 个移动分配(或您的 swap
所做的任何事情)。而适当的 operator=
重载可以直接使用给定的引用。
并且这是假设您无法编写实际作业的最佳版本。考虑 copy-assigning 一个 vector
到另一个。如果目标 vector
有足够的存储空间来容纳源向量的大小......你不需要分配。而如果您复制 construct,则必须分配存储空间。只有这样才能释放您本可以使用的存储空间。
即使在最好的情况下,您的 copy/move&swap 也不会比使用值 更有效。毕竟,您将引用参数; std::swap
不适用于值。因此,无论您认为使用引用会失去什么效率,都会以任何一种方式失去。
支持 copy/move&swap 的主要论点是:
减少代码重复。这只有在您的 copy/move 赋值操作的实现与 copy/move 构造或多或少相同时才有用。许多类型并非如此。如前所述,
vector
可以通过尽可能使用现有存储来优化自身。事实上很多容器都可以(特别是序列容器)。以最小的努力提供强大的异常保证。假设你的移动构造函数是 noexcept.
就个人而言,我更愿意完全避免这种情况。我更喜欢让编译器生成我所有的特殊成员函数。而如果一个类型绝对需要我写那些特殊的成员函数,那么这个类型将尽可能地精简。也就是说,它的唯一目的是管理需要此操作的任何内容。
这样我就不用担心了。我的大部分 类 不需要显式定义这些函数中的任何一个。
我知道这有一个公认的答案,但我觉得我必须跳进去。这里有两个不同的问题:
- 统一赋值运算符。这意味着您有一个按值获取的赋值运算符,而不是两个由 const & 和 && 获取的重载。
- 复制和交换 (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的动机是为了获得强异常保证,尽管T.C。在评论中指出,你可以这样做:
T& operator=(const T& t) { *this = T(t); return *this;}
这可能更多 efficient.In 我编写的大部分代码,性能是一个问题,我从来不需要强大的异常保证,所以我几乎永远不会这样做,所以这取决于你用例。
你永远不应该做 1。它们最好是单独的函数,这样移动赋值就可以标记为 noexcept。