复制消除
省略复制及移动 (C++11 起)构造函数,导致零复制的按值传递语义。
解释
纯右值语义(“有保证的复制消除”)从 C++17 起,非必须不会将纯右值实质化,并且它会被直接构造到其最终目标的存储中。这有时候意味着,即便语言的语法看起来进行了复制/移动(例如复制初始化),也并不进行复制/移动——这表示该类型完全不需要具有可访问的复制/移动构造函数。其例子包括: T f() { return U(); // 构造一个 U 类型的临时量,然后从临时量初始化返回的 T } T g() { return T(); // 直接构造返回的 T;没有移动 }
T x = T(T(f())); // 直接以 f() 的结果初始化 x;没有移动
struct C { /* ... */ }; C f(); struct D; D g(); struct D : C { D() : C(f()) {} // 初始化基类子对象时无消除 D(int) : D(g()) {} // 无消除,因为正在初始化的 D 对象可能是某个其他类的基类子对象 }; 注意:上述规则指定的不是优化,并且标准并未正式将其描述为“复制消除”(因为并无被消除的东西)。针对纯右值和临时量的 C++17 核心语言规定在本质上不同于之前的 C++ 版本:不再有用于复制/移动的临时量。描述 C++17 机制的另一种方式是“未实质化的值传递”或“延迟临时量实质化”:返回并使用纯右值时不实质化临时量。 |
(C++17 起) |
非强制的复制/移动 (C++11 起)操作消除
下列情形下,允许但不要求编译器省略类对象的复制和移动 (C++11 起)构造,即使复制/移动 (C++11 起)构造函数和析构函数拥有可观察的副作用。这些对象将直接构造到它们本来要复制/移动到的存储中。这是一项优化:即使进行了优化而不调用复制/移动 (C++11 起)构造函数,它仍然必须存在且可访问(如同完全未发生优化),否则程序非良构:
- return 语句中,当操作数是拥有自动存储期的非 volatile 对象的名字,该名字不是函数形参或处理块形参,且其具有与函数返回类型相同(忽略 cv 限定)的类类型时。这种复制消除的变体被称为 NRVO,“具名返回值优化”。
|
(C++17 前) |
(C++11 起) | |
|
(C++20 起) |
进行复制消除时,实现将被省略的复制/移动 (C++11 起)操作的源和目标单纯地当做指代同一对象的两种不同方式,而该对象将在假如不进行优化时两个对象中后被销毁的对象销毁时销毁(但如果被选择的构造函数的形参是对象类型的右值引用,那么该销毁发生于目标对象本应被销毁时) (C++11 起)。
可以连锁多次复制消除,以消除多次复制。
struct A { void* p; constexpr A() : p(this) {} A(const A&); // 禁用可平凡复制性 }; constexpr A a; // OK: a.p 指向 a constexpr A f() { A x; return x; } constexpr A b = f(); // 错误:b.p 会悬垂,并会指向 f 中的 x constexpr A c = A(); // (C++17 前) error: c.p 将悬垂并指向临时量 // (C++17 起) OK: c.p 指向 c; 不涉及临时量 |
(C++11 起) |
注解
复制消除是允许改变可观察副作用的唯一得到允许的优化形式 (C++14 前)两种允许的优化形式之一,另一种是分配消除与扩展 (C++14 起)。因为一些编译器并不在所有允许的场合中进行复制消除(例如调试模式下),依赖于复制/移动构造函数和析构函数的副作用的程序是不可移植的。
在 return 语句或 throw 表达式中,如果编译器不能进行复制消除,但满足或者(若非源是函数形参)本应满足复制消除的条件,那么即使源操作数由左值代表,编译器也将尝试使用移动构造函数 (C++23 前)就会将源操作数当做右值 (C++23 起);细节见 return 语句。 |
(C++11 起) |
功能特性测试宏 | 值 | 标准 | 功能特性 |
---|---|---|---|
__cpp_guaranteed_copy_elision |
201606L | (C++17) | 通过简化的值类别提供有保证的复制消除 |
示例
#include <iostream> struct Noisy { Noisy() { std::cout << "在 " << this << " 构造" << '\n'; } Noisy(const Noisy&) { std::cout << "复制构造\n"; } Noisy(Noisy&&) { std::cout << "移动构造\n"; } ~Noisy() { std::cout << "在 " << this << " 析构" << '\n'; } }; Noisy f() { Noisy v = Noisy(); // (C++17 前) 从临时量初始化 v 时发生复制消除,可能调用移动构造函数 // (C++17 起) "有保证的复制消除" return v; // 从 v 到结果对象的复制消除,可能调用移动构造函数 } void g(Noisy arg) { std::cout << "&arg = " << &arg << '\n'; } int main() { Noisy v = f(); // (C++17 前) 从 f() 的结果初始化 v 时发生复制消除 // (C++17 起) "有保证的复制消除" std::cout << "&v = " << &v << '\n'; g(f()); // (C++17 前) 从 f() 的结果初始化实参时发生复制消除 // (C++17 起) "有保证的复制消除" }
可能的输出:
在 0x7fffd635fd4e 构造 &v = 0x7fffd635fd4e 在 0x7fffd635fd4f 构造 &arg = 0x7fffd635fd4f 在 0x7fffd635fd4f 析构 在 0x7fffd635fd4e 析构
缺陷报告
下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。
缺陷报告 | 应用于 | 出版时的行为 | 正确行为 |
---|---|---|---|
CWG 1967 | C++11 | 在通过移动构造函数完成复制消除时依然会考虑被移动的对象的生存期 | 不考虑 |
CWG 2022 | C++11 | 常量求值中复制消除曾是可选的 | 常量求值中强制复制消除 |
CWG 2278 | C++11 | 常量求值中曾强制复制消除 | 常量求值中禁止复制消除 |
CWG 2426 | C++17 | 返回纯右值时不要求析构函数 | 潜在调用析构函数 |