复制省略
当满足某些条件时,可以省略从同类型(忽略 cv-限定符)源对象创建类对象的过程,即使所选的构造函数和/或析构函数具有副作用。这种省略对象创建的过程称为 副本消除。
目录 |
[编辑] 解释
副本消除在以下情况下是被允许的(可以组合以消除多个副本):
- 在具有类返回类型的函数中的 return 语句中,当操作数是非 volatile 且具有 自动存储期 的对象 obj 的名称时(函数参数或 处理函数 参数除外),可以通过将 obj 直接构造到函数调用的结果对象中来省略结果对象的 复制初始化。这种副本消除的变体称为 命名返回值优化 (NRVO)。
|
(C++17 前) |
(C++11 起) | |
|
(C++20 起) |
当发生副本消除时,实现将省略初始化的源和目标视为引用同一对象的两种不同方式。
销毁发生在没有优化的情况下,这两个对象本应被销毁的时间中较晚的一个。 |
(C++11 前) |
如果所选构造函数的第一个参数是对对象类型的右值引用,则该对象的销毁发生在目标本应被销毁时。否则,销毁发生在没有优化的情况下,这两个对象本应被销毁的时间中较晚的一个。 |
(C++11 起) |
纯右值语义(“保证的副本消除”)自 C++17 起,纯右值在需要时才实例化,然后直接构造到其最终目的地的存储中。这有时意味着即使语言语法在视觉上暗示复制/移动(例如 复制初始化),也没有执行复制/移动——这意味着该类型根本不需要可访问的复制/移动构造函数。示例包括: T f() { return U(); // constructs a temporary of type U, // then initializes the returned T from the temporary } T g() { return T(); // constructs the returned T directly; no move }
T x = T(T(f())); // x is initialized by the result of f() directly; no move
struct C { /* ... */ }; C f(); struct D; D g(); struct D : C { D() : C(f()) {} // no elision when initializing a base class subobject D(int) : D(g()) {} // no elision because the D object being initialized might // be a base-class subobject of some other class }; 注意:此规则未指定优化,标准也未正式将其描述为“副本消除”(因为没有内容被消除)。相反,C++17 核心语言对 纯右值 和 临时对象 的规范与早期 C++ 版本根本不同:不再有临时对象可供复制/移动。描述 C++17 机制的另一种方式是“未实例化值传递”或“延迟临时实例化”:纯右值在不实例化临时对象的情况下返回和使用。 |
(C++17 起) |
[编辑] 注意
副本消除是 唯一允许的优化形式(C++14 前) 两种允许的优化形式之一,与 分配消除和扩展 并列,(C++14 起) 可以改变可观察的副作用。由于某些编译器在允许的所有情况下都不执行副本消除(例如,在调试模式下),因此依赖于复制/移动构造函数和析构函数的副作用的程序不可移植。
在 return 语句或 throw 表达式中,如果编译器无法执行副本消除但满足副本消除的条件,或者除了源是函数参数之外,其他条件都满足,编译器将尝试使用移动构造函数,即使源操作数由左值指定(C++23 前) 源操作数将被视为右值(C++23 起);详见 return 语句。 struct A { void* p; constexpr A() : p(this) {} A(const A&); // Disable trivial copyability }; constexpr A a; // OK: a.p points to a constexpr A f() { A x; return x; } constexpr A b = f(); // error: b.p would be dangling and point to the x inside f constexpr A c = A(); // (until C++17) error: c.p would be dangling and point to a temporary // (since C++17) OK: c.p points to c; no temporary is involved |
(C++11 起) |
功能测试宏 | 值 | 标准 | 特性 |
---|---|---|---|
__cpp_guaranteed_copy_elision |
201606L |
(C++17) | 通过简化的 值类别 保证副本消除 |
[编辑] 示例
#include <iostream> struct Noisy { Noisy() { std::cout << "constructed at " << this << '\n'; } Noisy(const Noisy&) { std::cout << "copy-constructed\n"; } Noisy(Noisy&&) { std::cout << "move-constructed\n"; } ~Noisy() { std::cout << "destructed at " << this << '\n'; } }; Noisy f() { Noisy v = Noisy(); // (until C++17) copy elision initializing v from a temporary; // the move constructor may be called // (since C++17) "guaranteed copy elision" return v; // copy elision ("NRVO") from v to the result object; // the move constructor may be called } void g(Noisy arg) { std::cout << "&arg = " << &arg << '\n'; } int main() { Noisy v = f(); // (until C++17) copy elision initializing v from the result of f() // (since C++17) "guaranteed copy elision" std::cout << "&v = " << &v << '\n'; g(f()); // (until C++17) copy elision initializing arg from the result of f() // (since C++17) "guaranteed copy elision" }
可能的输出
constructed at 0x7fffd635fd4e &v = 0x7fffd635fd4e constructed at 0x7fffd635fd4f &arg = 0x7fffd635fd4f destructed at 0x7fffd635fd4f destructed at 0x7fffd635fd4e
[编辑] 缺陷报告
下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。
缺陷报告 | 应用于 | 发布时的行为 | 正确的行为 |
---|---|---|---|
CWG 1967 | C++11 | 当使用移动构造函数完成副本消除时, 被移动对象的生命周期仍被考虑在内。 |
不考虑 |
CWG 2426 | C++17 | 返回纯右值时不需要析构函数 | 析构函数可能被调用 |
CWG 2930 | C++98 | 只能消除复制(/移动)操作,但 复制初始化可以选择非复制(/移动)构造函数 |
消除任何对象构造 相关复制初始化 |