命名空间
变体
操作

复制省略

来自 cppreference.com
< cpp‎ | 语言
 
 
C++ 语言
 
 

省略复制和移动(自 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
}
返回类型的析构函数必须在 return 语句处可访问且未删除,即使没有销毁 T 对象。
  • 在初始化对象时,当初始化表达式是与变量类型相同类类型(忽略 cv 限定)的 右值
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,“命名返回值优化”。
  • 在对象初始化过程中,如果源对象是无名临时对象且与目标对象的类类型相同(忽略 cv 限定符),并且无名临时对象是 return 语句的操作数,则这种复制省略的变体被称为 URVO(无名返回值优化)。(在 C++17 及更高版本中,URVO 是强制性的,不再被视为复制省略的一种形式;参见上文。)
(直到 C++17)
  • throw 表达式中,如果操作数是非易失性对象的名称,该对象具有自动存储期且不是函数参数或处理程序参数,并且其作用域不超出最内层的 try 块(如果存在)。
  • 在处理程序中,如果参数与抛出的异常对象的类型相同(忽略 cv 限定符),则省略对异常对象的复制,处理程序主体直接访问异常对象,就好像它是通过引用捕获的(无法从异常对象中进行移动,因为它始终是左值)。如果这种复制省略会以任何原因改变程序的可观察行为(例如,如果修改了处理程序参数,并且使用 throw 重新抛出了异常对象),则会禁用它。
(自 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 返回右值时不需要析构函数 可能调用析构函数

[edit] 另请参阅