移动赋值运算符
移动赋值运算符是一个非模板非静态成员函数,其名称为 operator=,可以接受相同类类型的参数,并复制参数的内容,可能会修改参数。
目录 |
[编辑] 语法
有关正式的移动赋值运算符语法,请参阅函数声明。下面的语法列表仅演示了所有有效移动赋值运算符语法的一个子集。
返回类型 operator=( 参数列表 ); |
(1) | ||||||||
返回类型 operator=( 参数列表 ) 函数体 |
(2) | ||||||||
返回类型 operator=( 无默认参数列表 ) = default; |
(3) | ||||||||
返回类型 operator=( 参数列表 ) = delete; |
(4) | ||||||||
返回类型 类名 :: operator=( 参数列表 ) 函数体 |
(5) | ||||||||
返回类型 类名 :: operator=( 无默认参数列表 ) = default; |
(6) | ||||||||
类名 | - | 正在声明其移动赋值运算符的类,在下面的描述中,类类型以 T 表示 |
参数列表 | - | 仅包含一个参数的参数列表,该参数的类型为 T&& 、const T&&、volatile T&& 或 const volatile T&& |
无默认参数列表 | - | 仅包含一个参数的参数列表,该参数的类型为 T&& 、const T&&、volatile T&& 或 const volatile T&& 且不具有默认实参 |
函数体 | - | 移动赋值运算符的函数体 |
返回类型 | - | 任何类型,但为了与 Scala 类型保持一致,推荐使用 T& |
[编辑] 解释
struct X { X& operator=(X&& other); // move assignment operator // X operator=(const X other); // Error: incorrect parameter type }; union Y { // move assignment operators can have syntaxes not listed above, // as long as they follow the general function declaration syntax // and do not viloate the restrictions listed above auto operator=(Y&& other) -> Y&; // OK: trailing return type Y& operator=(this Y&& self, Y& other); // OK: explicit object parameter // Y& operator=(Y&&, int num = 1); // Error: has other non-object parameters };
当重载决议选中移动赋值运算符时,它会被调用,例如,当一个对象出现在赋值表达式的左侧,而右侧是相同或隐式可转换类型的右值时。
移动赋值运算符通常会转移参数持有的资源(例如,指向动态分配对象的指针、文件描述符、TCP 套接字、线程句柄等),而不是复制它们,并使参数处于某种有效但未确定的状态。由于移动赋值不会改变参数的生命周期,因此析构函数通常会在稍后对参数进行调用。例如,从 std::string 或 std::vector 进行移动赋值可能会导致参数为空。移动赋值的定义比普通赋值的限制更少,而不是更多;普通赋值在完成时必须留下两份数据副本,而移动赋值只需要留下一份。
[编辑] 隐式声明的移动赋值运算符
如果对于类类型没有提供用户定义的移动赋值运算符,并且以下所有条件都为真:
那么编译器将声明一个移动赋值运算符作为其类的 inline public 成员,其签名为 T& T::operator=(T&&)。
一个类可以有多个移动赋值运算符,例如 T& T::operator=(const T&&) 和 T& T::operator=(T&&)。如果存在一些用户定义的移动赋值运算符,用户仍然可以使用关键字 default
强制生成隐式声明的移动赋值运算符。
隐式声明的移动赋值运算符具有异常规范,如 动态异常规范(C++17 前)noexcept 规范(C++17 起) 中所述。
由于对于任何类总是声明了某个赋值运算符(移动或复制),因此基类赋值运算符总是被隐藏。如果使用 using 声明将基类中的赋值运算符引入,并且其参数类型可能与派生类的隐式赋值运算符的参数类型相同,则 using 声明也会被隐式声明隐藏。
[编辑] 隐式定义的移动赋值运算符
如果隐式声明的移动赋值运算符既不被删除也不平凡,那么当它被odr-使用或需要用于常量求值(C++14 起)时,编译器会定义它(即生成并编译函数体)。
对于联合类型,隐式定义的移动赋值运算符复制对象表示(如同通过 std::memmove)。
对于非联合类类型,移动赋值运算符按照声明顺序对其对象的直接基类和即时非静态成员执行完全成员级移动赋值,对标量使用内置赋值,对数组使用成员级移动赋值,对类类型使用移动赋值运算符(非虚调用)。
如果满足以下条件,则类
|
(C++14 起) (直至 C++23) |
类 |
(C++23 起) |
与复制赋值一样,隐式定义的移动赋值运算符是否多次赋值通过继承层次结构中多于一个路径可访问的虚基类子对象是未指定的。
struct V { V& operator=(V&& other) { // this may be called once or twice // if called twice, 'other' is the just-moved-from V subobject return *this; } }; struct A : virtual V {}; // operator= calls V::operator= struct B : virtual V {}; // operator= calls V::operator= struct C : B, A {}; // operator= calls B::operator=, then A::operator= // but they may only call V::operator= once int main() { C c1, c2; c2 = std::move(c1); }
[编辑] 已删除的移动赋值运算符
类 T
的隐式声明或默认的移动赋值运算符在满足以下任何条件时被定义为已删除:
-
T
具有 const 限定的非类类型(或可能是多维数组)的非静态数据成员。 -
T
具有引用类型的非静态数据成员。 -
T
具有类类型M
(或可能是多维数组)的潜在构造子对象,以至于应用于查找M
的移动赋值运算符的重载决议
- 未产生可用的候选,或者
- 在子对象是变体成员的情况下,选择了一个非平凡函数。
已删除的隐式声明移动赋值运算符在重载决议中被忽略。
[编辑] 平凡移动赋值运算符
如果满足以下所有条件,类 T
的移动赋值运算符是平凡的:
- 它不是用户提供的(意味着它是隐式定义或默认的);
-
T
没有虚成员函数; -
T
没有虚基类; - 为
T
的每个直接基类选择的移动赋值运算符是平凡的; - 为
T
的每个非静态类类型(或类类型数组)成员选择的移动赋值运算符是平凡的。
平凡移动赋值运算符执行与平凡复制赋值运算符相同的操作,即复制对象表示,如同通过 std::memmove。所有与 C 语言兼容的数据类型都是可平凡移动赋值的。
[编辑] 合格的移动赋值运算符
如果移动赋值运算符未被删除,则它是合格的。 |
(C++20 前) |
如果满足以下所有条件,移动赋值运算符是合格的: |
(C++20 起) |
合格移动赋值运算符的平凡性决定了该类是否是可平凡复制类型。
[编辑] 注意
如果同时提供了复制和移动赋值运算符,当参数是右值(无论是像无名临时对象这样的纯右值,还是像 std::move 结果这样的将亡值)时,重载决议选择移动赋值;当参数是左值(命名对象或返回左值引用的函数/运算符)时,选择复制赋值。如果只提供了复制赋值,所有参数类别都会选择它(只要它通过值或作为 const 引用获取参数,因为右值可以绑定到 const 引用),这使得复制赋值在移动不可用时成为移动赋值的备选方案。
隐式定义的移动赋值运算符是否多次赋值通过继承层次结构中多于一个路径可访问的虚基类子对象是未指定的(这同样适用于复制赋值)。
有关用户定义的移动赋值运算符的预期行为的更多详细信息,请参阅赋值运算符重载。
[编辑] 示例
#include <iostream> #include <string> #include <utility> struct A { std::string s; A() : s("test") {} A(const A& o) : s(o.s) { std::cout << "move failed!\n"; } A(A&& o) : s(std::move(o.s)) {} A& operator=(const A& other) { s = other.s; std::cout << "copy assigned\n"; return *this; } A& operator=(A&& other) { s = std::move(other.s); std::cout << "move assigned\n"; return *this; } }; A f(A a) { return a; } struct B : A { std::string s2; int n; // implicit move assignment operator B& B::operator=(B&&) // calls A's move assignment operator // calls s2's move assignment operator // and makes a bitwise copy of n }; struct C : B { ~C() {} // destructor prevents implicit move assignment }; struct D : B { D() {} ~D() {} // destructor would prevent implicit move assignment D& operator=(D&&) = default; // force a move assignment anyway }; int main() { A a1, a2; std::cout << "Trying to move-assign A from rvalue temporary\n"; a1 = f(A()); // move-assignment from rvalue temporary std::cout << "Trying to move-assign A from xvalue\n"; a2 = std::move(a1); // move-assignment from xvalue std::cout << "\nTrying to move-assign B\n"; B b1, b2; std::cout << "Before move, b1.s = \"" << b1.s << "\"\n"; b2 = std::move(b1); // calls implicit move assignment std::cout << "After move, b1.s = \"" << b1.s << "\"\n"; std::cout << "\nTrying to move-assign C\n"; C c1, c2; c2 = std::move(c1); // calls the copy assignment operator std::cout << "\nTrying to move-assign D\n"; D d1, d2; d2 = std::move(d1); }
输出
Trying to move-assign A from rvalue temporary move assigned Trying to move-assign A from xvalue move assigned Trying to move-assign B Before move, b1.s = "test" move assigned After move, b1.s = "" Trying to move-assign C copy assigned Trying to move-assign D move assigned
[编辑] 缺陷报告
下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。
缺陷报告 | 应用于 | 发布时的行为 | 正确的行为 |
---|---|---|---|
CWG 1353 | C++11 | 默认的移动赋值运算符被定义为已删除的条件 未考虑多维数组类型 |
考虑这些类型 |
CWG 1402 | C++11 | 一个默认的移动赋值运算符会 调用一个非平凡的复制赋值运算符,然后被 删除;一个默认的移动赋值运算符 被删除后仍参与重载决议 |
允许调用此类 复制赋值 运算符;在重载决议中 被忽略 |
CWG 1806 | C++11 | 涉及虚基类的默认移动赋值运算符的规范 缺失 |
已添加 |
CWG 2094 | C++11 | 一个 volatile 子对象导致默认的 移动赋值运算符变为非平凡(CWG 问题 496) |
平凡性不受影响 |
CWG 2180 | C++11 | 如果类 T 是抽象的并且具有不可移动赋值的直接虚基类,则类 T 的默认移动赋值运算符未被定义为已删除 |
在这种情况下,该运算符 被定义为已删除 |
CWG 2595 | C++20 | 如果存在另一个移动赋值运算符,其约束更强 但未满足其关联约束,则移动赋值运算符不合格 在这种情况下它可以合格 |
它在这种情况下可以合格 |
CWG 2690 | C++11 | 联合类型的隐式定义移动赋值运算符 未复制对象表示 |
它们复制对象 表示 |