移动构造函数
移动构造函数是一个 构造函数,它可以被调用,其参数类型与该类相同,并且复制参数的内容,这可能会改变参数。
内容 |
[编辑] 语法
类名 ( 参数列表 ); |
(1) | ||||||||
类名 ( 参数列表 ) 函数体 |
(2) | ||||||||
类名 ( 单参数列表 ) = default; |
(3) | ||||||||
类名 ( 参数列表 ) = delete; |
(4) | ||||||||
类名 :: 类名 ( 参数列表 ) 函数体 |
(5) | ||||||||
类名 :: 类名 ( 单参数列表 ) = default; |
(6) | ||||||||
类名 | - | 声明移动构造函数的类 |
参数列表 | - | 非空的 参数列表,满足以下所有条件
|
单参数列表 | - | 仅包含一个参数的 参数列表,该参数的类型为 T&&,const T&&,volatile T&& 或 const volatile T&&,并且没有默认参数 |
函数体 | - | 移动构造函数的 函数体 |
[编辑] 解释
struct X { X(X&& other); // move constructor // X(X other); // Error: incorrect parameter type }; union Y { Y(Y&& other, int num = 1); // move constructor with multiple parameters // Y(Y&& other, int num); // Error: `num` has no default argument };
移动构造函数通常在对象被初始化(通过直接初始化或复制初始化)时被调用,来自右值(右值引用或纯右值)(直到 C++17)右值引用(从 C++17 开始),包括
- 初始化:T a = std::move(b); 或 T a(std::move(b));,其中 b 为
T
类型; - 函数参数传递:f(std::move(a));,其中 a 为
T
类型,而 f 为 void f(T t); - 函数返回值:return a; 在类似于 T f() 的函数中,其中 a 为具有移动构造函数的
T
类型。
当初始化器为纯右值时,移动构造函数调用通常会被优化掉(直到 C++17)永远不会发生(从 C++17 开始),请参阅复制省略。
移动构造函数通常会转移参数所持有的资源(例如指向动态分配对象的指针、文件描述符、TCP 套接字、线程句柄等),而不是创建它们的副本,并将参数置于某个有效但其他方面不确定的状态。由于移动构造函数不会改变参数的生命周期,因此析构函数通常会在以后对参数进行调用。例如,从std::string或std::vector移动可能会导致参数被清空。对于某些类型,例如std::unique_ptr,移动后的状态是完全指定的。
[edit] 隐式声明的移动构造函数
如果类类型没有提供任何用户定义的移动构造函数,并且以下所有条件都为真
那么编译器将声明一个移动构造函数为非显式 inline public 类成员,其签名为 T::T(T&&).
一个类可以有多个移动构造函数,例如 T::T(const T&&) 和 T::T(T&&)。如果存在一些用户定义的移动构造函数,用户仍然可以使用关键字 default 强制生成隐式声明的移动构造函数。
隐式声明的(或在其首次声明时被设置为默认值的)移动构造函数具有异常规范,如动态异常规范(直到 C++17)noexcept 规范(从 C++17 开始)中所述。
[edit] 隐式定义的移动构造函数
如果隐式声明的移动构造函数既没有被删除也没有被设置为平凡的,则如果ODR 使用或常量求值需要,它将由编译器定义(即生成函数体并编译)。对于联合类型,隐式定义的移动构造函数会复制对象表示(就像使用std::memmove一样)。对于非联合类类型,移动构造函数按其初始化顺序执行对对象的直接基类子对象和成员子对象的完全逐成员移动,使用带有右值引用参数的直接初始化。对于类类型的每个非静态数据成员,移动构造函数将引用绑定到源引用所绑定的同一个对象或函数。
如果这满足了constexpr
构造函数(直到 C++23)constexpr
函数(从 C++23 开始)的要求,则生成的移动构造函数为 constexpr.
[edit] 删除的移动构造函数
如果类 T
的隐式声明的或显式设置为默认值的移动构造函数被定义为删除,则如果 T
具有类类型 M
(或可能是其多维数组)的可能被构造的子对象,并且满足以下条件
-
M
的析构函数被删除或从复制构造函数中不可访问,或者 - 应用于查找
M
的移动构造函数的重载解析
- 不会导致可用的候选者,或者
- 在子对象为变体成员的情况下,会选择一个非平凡的函数。
这种构造函数会被重载解析忽略(否则它会阻止从右值复制初始化)。
[edit] 平凡的移动构造函数
如果满足以下所有条件,则类 T
的移动构造函数是平凡的
- 它不是用户提供的(意味着它是隐式定义的或设置为默认值的);
-
T
没有虚成员函数; -
T
没有虚基类; - 为
T
的每个直接基类选择的移动构造函数都是平凡的; - 为
T
的每个非静态类类型(或类类型数组)成员选择的移动构造函数都是平凡的。
平凡的移动构造函数是一种执行与平凡的复制构造函数相同操作的构造函数,即就像使用std::memmove一样创建对象表示的副本。所有与 C 语言兼容的数据类型都可以平凡地移动。
[edit] 合格的移动构造函数
如果移动构造函数没有被删除,则它就是合格的。 |
(直到 C++20) |
如果满足以下所有条件,则移动构造函数是合格的 |
(从 C++20 开始) |
合格移动构造函数的平凡性决定了该类是否为隐式生命周期类型,以及该类是否为平凡可复制类型。
[edit] 备注
为了使强异常安全保证成为可能,用户定义的移动构造函数不应该抛出异常。例如,std::vector依赖于std::move_if_noexcept 在元素需要重新分配时选择移动或复制。
如果同时提供了复制构造函数和移动构造函数,并且没有其他构造函数可用,则如果参数为相同类型的右值(例如 std::move 的结果或无名临时对象之类的纯右值(直到 C++17)),重载解析会选择移动构造函数,如果参数为左值(命名对象或返回左值引用的函数/运算符),则会选择复制构造函数。如果只提供了复制构造函数,则所有参数类别都会选择它(只要它接受对常量的引用,因为右值可以绑定到常量引用),这使得复制成为移动的回退,当移动不可用时。
[edit] 示例
#include <iomanip> #include <iostream> #include <string> #include <utility> struct A { std::string s; int k; A() : s("test"), k(-1) {} A(const A& o) : s(o.s), k(o.k) { std::cout << "move failed!\n"; } A(A&& o) noexcept : s(std::move(o.s)), // explicit move of a member of class type k(std::exchange(o.k, 0)) // explicit move of a member of non-class type {} }; A f(A a) { return a; } struct B : A { std::string s2; int n; // implicit move constructor B::(B&&) // calls A's move constructor // calls s2's move constructor // and makes a bitwise copy of n }; struct C : B { ~C() {} // destructor prevents implicit move constructor C::(C&&) }; struct D : B { D() {} ~D() {} // destructor would prevent implicit move constructor D::(D&&) D(D&&) = default; // forces a move constructor anyway }; int main() { std::cout << "Trying to move A\n"; A a1 = f(A()); // return by value move-constructs the target // from the function parameter std::cout << "Before move, a1.s = " << std::quoted(a1.s) << " a1.k = " << a1.k << '\n'; A a2 = std::move(a1); // move-constructs from xvalue std::cout << "After move, a1.s = " << std::quoted(a1.s) << " a1.k = " << a1.k << '\n'; std::cout << "\nTrying to move B\n"; B b1; std::cout << "Before move, b1.s = " << std::quoted(b1.s) << "\n"; B b2 = std::move(b1); // calls implicit move constructor std::cout << "After move, b1.s = " << std::quoted(b1.s) << "\n"; std::cout << "\nTrying to move C\n"; C c1; C c2 = std::move(c1); // calls copy constructor std::cout << "\nTrying to move D\n"; D d1; D d2 = std::move(d1); }
输出
Trying to move A Before move, a1.s = "test" a1.k = -1 After move, a1.s = "" a1.k = 0 Trying to move B Before move, b1.s = "test" After move, b1.s = "" Trying to move C move failed! Trying to move D
[edit] 缺陷报告
以下行为更改缺陷报告被追溯应用于以前发布的 C++ 标准。
DR | 应用于 | 已发布的行为 | 正确的行为 |
---|---|---|---|
CWG 1353 | C++11 | 将默认移动构造函数定义为删除的条件 没有考虑多维数组类型 |
考虑这些类型 |
CWG 1402 | C++11 | 将调用 非平凡的复制构造函数的默认移动构造函数 被定义为删除;被删除的默认移动构造函数 仍然参与重载解析 |
允许调用此类 构造函数;在重载解析中被忽略 中被忽略 |
CWG 1491 | C++11 | 具有非静态数据的类的默认移动构造函数被定义为已删除。 右值引用类型成员被定义为已删除。 |
在这种情况下没有被删除。 |
CWG 2094 | C++11 | 一个易变的子对象被设置为默认移动构造函数。 移动构造函数非平凡 ( CWG issue 496 ) |
平凡性不受影响。 |
CWG 2595 | C++20 | 如果存在另一个移动构造函数,则移动构造函数将不可用。 另一个移动构造函数约束性更强, 但不能满足其关联的约束。 |
在这种情况下,它可以被使用。 |