Move constructors
移动构造函数是一种 构造函数 ,它可以通过传入同类型参数调用,并复制参数内容,同时可能改变参数状态。
目录 |
语法
类名
(
参数列表
);
|
(1) | ||||||||
类名
(
参数列表
)
函数体
|
(2) | ||||||||
类名
(
单参数列表
) = default;
|
(3) | ||||||||
类名
(
参数列表
) = delete;
|
(4) | ||||||||
类名
::
类名
(
参数列表
)
函数体
|
(5) | ||||||||
类名
::
类名
(
单参数列表
) = default;
|
(6) | ||||||||
| class-name | - | 声明移动构造函数的类名 |
| parameter-list | - |
满足以下所有条件的非空
参数列表
:
|
| single-parameter-list | - | 仅包含一个参数的 参数列表 ,该参数类型为 T && 、 const T && 、 volatile T && 或 const volatile T && ,且不具有默认实参 |
| function-body | - | 移动构造函数的 函数体 |
说明
struct X { X(X&& other); // 移动构造函数 // X(X other); // 错误:参数类型不正确 }; union Y { Y(Y&& other, int num = 1); // 带多个参数的移动构造函数 // Y(Y&& other, int num); // 错误:`num` 缺少默认参数 };
移动构造函数通常在对象从相同类型的 右值 (xvalue 或 prvalue) (C++17 前) xvalue (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 ) ; -
函数返回:在函数
T f
(
)
中执行
return
a
;
,其中
a
是具有移动构造函数的
T类型。
当初始化器为纯右值时,移动构造函数的调用 通常会被优化掉 (C++17 前) 永远不会发生 (C++17 起) ,详见 复制消除 。
移动构造函数通常转移参数所持有的资源(例如指向动态分配对象的指针、文件描述符、TCP套接字、线程句柄等),而不是创建它们的副本,并使参数处于某种有效但不确定的状态。由于移动构造函数不会改变参数的生命周期,析构函数通常会在稍后阶段被调用。例如,从 std::string 或 std::vector 移动可能导致参数变为空。对于某些类型,例如 std::unique_ptr ,被移动后的状态有完整规范。
隐式声明的移动构造函数
如果类类型未提供任何用户定义的移动构造函数,且满足以下所有条件:
随后编译器会声明一个移动构造函数作为其类的非 显式 inline public 成员,其签名为 T :: T ( T && ) 。
一个类可以拥有多个移动构造函数,例如同时存在 T :: T ( const T && ) 和 T :: T ( T && ) 。如果已存在用户定义的移动构造函数,用户仍可通过 default 关键字强制生成隐式声明的移动构造函数。
隐式声明(或在其首次声明时默认生成)的移动构造函数具有异常规范,具体描述见 动态异常规范 (直至 C++17) noexcept 规范 (自 C++17 起) 。
隐式定义的移动构造函数
如果隐式声明的移动构造函数既未被删除也非平凡,当遇到 ODR使用 或 常量求值需要 时,编译器会定义(即生成并编译函数体)该构造函数。对于联合体类型,隐式定义的移动构造函数会复制对象表示(如同通过 std::memmove )。对于非联合体类类型,移动构造函数按照初始化顺序,以 xvalue 实参进行直接初始化,对对象的直接基类子对象和非静态数据成员子对象执行逐成员移动。对于引用类型的每个非静态数据成员,移动构造函数会将引用绑定到源引用所绑定的相同对象或函数。
如果这满足
constexpr
构造函数
(C++23 前)
constexpr
函数
(C++23 起)
的要求,则生成的移动构造函数为
constexpr
。
已删除的移动构造函数
当类
T
具有类类型
M
(或其可能的多维数组)的
潜在构造子对象
,且满足以下条件时,隐式声明或显式默认的移动构造函数将被定义为弃置函数:
-
M的析构函数被删除或无法从拷贝构造函数访问,或 -
应用于查找
M的移动构造函数的重载决议
-
- 不会产生可用的候选函数,或
- 在子对象为 variant member 的情况下,选择了非平凡函数。
这样的构造函数会被 重载决议 忽略(否则它会阻止来自右值的复制初始化)。
平凡移动构造函数
当满足以下所有条件时,类
T
的移动构造函数是平凡的:
- 它不是用户提供的(即它是隐式定义或默认的);
-
T没有虚成员函数; -
T没有虚基类; -
为
T的每个直接基类选择的移动构造函数是平凡的; -
为
T的每个非静态类类型(或类类型数组)成员选择的移动构造函数是平凡的。
平凡移动构造函数是一种执行与平凡拷贝构造函数相同操作的构造函数,即通过类似 std::memmove 的方式复制对象表示。所有与C语言兼容的数据类型都是可平凡移动的。
合格的移动构造函数
|
若移动构造函数未被删除,则其具备资格。 |
(C++20 前) |
|
若满足下列所有条件,则移动构造函数具备资格: |
(C++20 起) |
合格移动构造函数的平凡性决定了该类是否为 隐式生存期类型 ,以及该类是否为 可平凡复制类型 。
注释
为实现 强异常保证 ,用户定义的移动构造函数不应抛出异常。例如,当需要重新安置元素时, std::vector 依赖 std::move_if_noexcept 在移动和拷贝之间进行选择。
如果同时提供了拷贝构造函数和移动构造函数,且没有其他可行的构造函数,当实参为同类型的 右值 时(例如 std::move 产生的 亡值 ,或是无名临时对象这样的 纯右值 (C++17 前) ),重载决议将选择移动构造函数;当实参为 左值 时(具名对象或返回左值引用的函数/运算符),则选择拷贝构造函数。如果仅提供了拷贝构造函数,所有实参类别都会选择它(只要该构造函数接受const引用参数,因为右值可以绑定到const引用),这使得在移动操作不可用时,拷贝操作成为移动的备用方案。
示例
#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)), // 显式移动类类型成员 k(std::exchange(o.k, 0)) // 显式移动非类类型成员 {} }; A f(A a) { return a; } struct B : A { std::string s2; int n; // 隐式移动构造函数 B::(B&&) // 调用 A 的移动构造函数 // 调用 s2 的移动构造函数 // 并对 n 进行位拷贝 }; struct C : B { ~C() {} // 析构函数阻止隐式移动构造函数 C::(C&&) }; struct D : B { D() {} ~D() {} // 析构函数本会阻止隐式移动构造函数 D::(D&&) D(D&&) = default; // 强制生成移动构造函数 }; int main() { std::cout << "Trying to move A\n"; A a1 = f(A()); // 通过返回值从函数参数移动构造目标对象 // std::cout << "Before move, a1.s = " << std::quoted(a1.s) << " a1.k = " << a1.k << '\n'; A a2 = std::move(a1); // 从 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); // 调用隐式移动构造函数 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); // 调用拷贝构造函数 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
缺陷报告
以下行为变更缺陷报告被追溯应用于先前发布的C++标准。
| 缺陷报告 | 应用于 | 发布时的行为 | 正确行为 |
|---|---|---|---|
| CWG 1353 | C++11 |
默认移动构造函数被定义为删除的条件
未考虑多维数组类型 |
考虑这些类型 |
| CWG 1402 | C++11 |
会调用非平凡拷贝构造函数的默认移动构造函数
被定义为删除;被删除的默认移动构造函数 仍参与重载决议 |
允许调用此类拷贝
构造函数;在重载决议中 将其忽略 |
| CWG 1491 | C++11 |
具有右值引用类型非静态数据成员的类
的默认移动构造函数被定义为删除 |
在此情况下不删除 |
| CWG 2094 | C++11 |
volatile 子对象使默认移动构造函数
变为非平凡( CWG 问题 496 ) |
平凡性不受影响 |
| CWG 2595 | C++20 |
若存在另一个更具约束但
不满足其关联约束的移动构造函数, 则当前移动构造函数不符合条件 |
在此情况下可以符合条件 |