复制省略
省略复制和移动(自 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++11 起)省略
在以下情况下,即使复制/移动(自 C++11 起)构造函数和析构函数具有可观察的副作用,编译器也被允许(但不是必须)省略类对象的复制和移动(自 C++11 起)构造。这些对象直接构造到它们本来要被复制/移动到的存储中。这是一个优化:即使它发生并且复制/移动(自 C++11 起)构造函数没有被调用,它仍然必须存在且可访问(就好像没有优化一样),否则程序将是非法的
- 在 return 语句中,当操作数是非易失对象的名称,该对象具有自动存储时长,不是函数参数或处理程序参数,并且与函数返回类型相同类类型(忽略 cv 限定)时。这种复制省略变体被称为 NRVO,“命名返回值优化”。
|
(直到 C++17) |
|
(自 C++11 起) |
|
(自 C++20 起) |
当发生复制省略时,实现会将省略的复制 /移动(自 C++11 起) 操作的源和目标视为对同一对象的两种不同引用方式,并且该对象的销毁将在两种情况下该对象本应被销毁的较晚时间发生 (除了,如果所选构造函数的参数是对对象类型的右值引用,则销毁将在目标本应被销毁时发生)(自 C++11 起)。
可以将多个复制省略链接起来,以消除多个复制。
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 起) |
[edit] 注意
复制省略是 唯一允许的优化形式(直到 C++14) 两种允许的优化形式之一,另一种是分配省略和扩展(自 C++14 起),它可以改变可观察的副作用。由于某些编译器并非在所有允许的情况下都执行复制省略(例如,在调试模式下),因此依赖复制/移动构造函数和析构函数副作用的程序不可移植。
在 return 语句或 throw 表达式中,如果编译器无法执行复制省略,但满足复制省略的条件,或者除了源是函数参数之外,其他条件都满足, 编译器将尝试使用移动构造函数,即使源操作数由左值指定(直到 C++23) 源操作数将被视为右值(自 C++23 起);有关详细信息,请参阅 return 语句。 |
(自 C++11 起) |
特性测试宏 | 值 | 标准 | 特性 |
---|---|---|---|
__cpp_guaranteed_copy_elision |
201606L | (C++17) | 通过简化的值类别保证复制省略 |
[edit] 示例
#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
[edit] 缺陷报告
以下行为变更缺陷报告已追溯应用于先前发布的 C++ 标准。
DR | 应用于 | 已发布的行为 | 正确行为 |
---|---|---|---|
CWG 1967 | C++11 | 当使用移动构造函数进行复制省略时, 移动来源对象的生存期仍然被认为是 |
不被认为是 |
CWG 2022 | C++11 | 在常量求值期间,复制省略是可选的 | 在常量求值期间,复制省略是强制性的 |
CWG 2278 | C++11 | 在常量求值期间,复制省略是强制性的 | 在常量求值期间,复制省略是被禁止的 |
CWG 2426 | C++17 | 返回右值时不需要析构函数 | 可能调用析构函数 |