Move assignment operator
移动赋值运算符是一个非模板 非静态成员函数 ,其名称为 operator = ,可通过相同类类型的参数调用,并复制参数内容(可能改变参数状态)。
目录 |
语法
关于正式的移动赋值运算符语法,请参阅 函数声明 。以下语法列表仅展示了所有有效移动赋值运算符语法的一部分。
返回类型
operator=(
参数列表
);
|
(1) | ||||||||
返回类型
operator=(
参数列表
)
函数体
|
(2) | ||||||||
返回类型
operator=(
无默认参数列表
) = default;
|
(3) | ||||||||
返回类型
operator=(
参数列表
) = delete;
|
(4) | ||||||||
返回类型
类名
::
operator=(
参数列表
)
函数体
|
(5) | ||||||||
返回类型
类名
::
operator=(
无默认参数列表
) = default;
|
(6) | ||||||||
| class-name | - |
正在声明移动赋值运算符的类,下文描述中类类型以
T
表示
|
| parameter-list | - |
仅包含一个参数的
参数列表
,参数类型为
T&&
、
const
T
&&
、
volatile
T
&&
或
const
volatile
T
&&
|
| parameter-list-no-default | - |
仅包含一个参数的
参数列表
,参数类型为
T&&
、
const
T
&&
、
volatile
T
&&
或
const
volatile
T
&&
,且不包含默认参数
|
| function-body | - | 移动赋值运算符的 函数体 |
| return-type | - |
任意类型,但为与标量类型保持一致,推荐使用
T&
|
说明
struct X { X& operator=(X&& other); // 移动赋值运算符 // X operator=(const X other); // 错误:参数类型不正确 }; union Y { // 移动赋值运算符可以采用未列出的语法形式, // 只要遵循常规函数声明语法 // 且不违反上述限制条件 auto operator=(Y&& other) -> Y&; // 正确:尾置返回类型 Y& operator=(this Y&& self, Y& other); // 正确:显式对象参数 // Y& operator=(Y&&, int num = 1); // 错误:存在其他非对象参数 };
移动赋值运算符在 重载决议 选中时被调用,例如当对象出现在赋值表达式左侧,且右侧为相同类型或可隐式转换类型的右值时。
移动赋值运算符通常转移参数所持有的资源(例如指向动态分配对象的指针、文件描述符、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) { // 此函数可能被调用一次或两次 // 若被调用两次,'other' 是刚被移动的 V 子对象 return *this; } }; struct A : virtual V {}; // operator= 会调用 V::operator= struct B : virtual V {}; // operator= 会调用 V::operator= struct C : B, A {}; // operator= 会先调用 B::operator=,再调用 A::operator= // 但它们可能只会调用一次 V::operator= int main() { C c1, c2; c2 = std::move(c1); }
已删除的移动赋值运算符
当满足以下任一条件时,类
T
隐式声明或默认的移动赋值运算符将被定义为删除:
-
T拥有一个 const 限定非类类型(或可能是其多维数组)的非静态数据成员。 -
T拥有一个引用类型的非静态数据成员。 -
T拥有一个类类型M(或可能是其多维数组)的 潜在构造子对象 ,该情况适用于查找M的移动赋值运算符时进行的重载解析
-
- 不会产生可用的候选函数,或
- 在该子对象为 variant member 的情况下,选择了非平凡函数。
被删除的隐式声明的移动赋值运算符会被 重载决议 忽略。
平凡移动赋值运算符
当满足以下所有条件时,类
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; // 隐式移动赋值运算符 B& B::operator=(B&&) // 调用 A 的移动赋值运算符 // 调用 s2 的移动赋值运算符 // 并对 n 进行位拷贝 }; struct C : B { ~C() {} // 析构函数阻止隐式移动赋值 }; struct D : B { D() {} ~D() {} // 析构函数本会阻止隐式移动赋值 D& operator=(D&&) = default; // 强制生成移动赋值运算符 }; int main() { A a1, a2; std::cout << "尝试从右值临时对象移动赋值 A\n"; a1 = f(A()); // 从右值临时对象进行移动赋值 std::cout << "尝试从 xvalue 移动赋值 A\n"; a2 = std::move(a1); // 从 xvalue 进行移动赋值 std::cout << "\n尝试移动赋值 B\n"; B b1, b2; std::cout << "移动前,b1.s = \"" << b1.s << "\"\n"; b2 = std::move(b1); // 调用隐式移动赋值 std::cout << "移动后,b1.s = \"" << b1.s << "\"\n"; std::cout << "\n尝试移动赋值 C\n"; C c1, c2; c2 = std::move(c1); // 调用拷贝赋值运算符 std::cout << "\n尝试移动赋值 D\n"; D d1, d2; d2 = std::move(d1); }
输出:
尝试从右值临时对象移动赋值 A move assigned 尝试从 xvalue 移动赋值 A move assigned 尝试移动赋值 B 移动前,b1.s = "test" move assigned 移动后,b1.s = "" 尝试移动赋值 C copy assigned 尝试移动赋值 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
为抽象类且具有不可移动赋值的直接虚基类时,未将默认移动赋值运算符定义为删除
|
此情况下运算符定义为删除 |
| CWG 2595 | C++20 | 当存在另一个约束更强但不满足其关联约束的移动赋值运算符时,当前移动赋值运算符不符合条件 | 此情况下可符合条件 |
| CWG 2690 | C++11 | 联合类型的隐式定义移动赋值运算符未复制对象表示 | 复制对象表示 |