命名空间
变体
操作

复制省略

来自 cppreference.cn
< cpp‎ | 语言
 
 
C++ 语言
表达式
替代表示
字面量
布尔字面量 - 整数字面量 - 浮点字面量
字符字面量 - 字符串字面量 - nullptr (C++11)
用户定义 (C++11)
工具
属性 (C++11)
类型
typedef 声明
类型别名声明 (C++11)
类型转换
内存分配
类特有的函数属性
explicit (C++11)
static

特殊成员函数
模板
杂项
 
 

当满足某些条件时,可以省略从同类型(忽略 cv-限定符)源对象创建类对象的过程,即使所选的构造函数和/或析构函数具有副作用。这种省略对象创建的过程称为 副本消除

目录

[编辑] 解释

副本消除在以下情况下是被允许的(可以组合以消除多个副本):

  • 在具有类返回类型的函数中的 return 语句中,当操作数是非 volatile 且具有 自动存储期 的对象 obj 的名称时(函数参数或 处理函数 参数除外),可以通过将 obj 直接构造到函数调用的结果对象中来省略结果对象的 复制初始化。这种副本消除的变体称为 命名返回值优化 (NRVO)。
  • 当类对象 target 使用尚未绑定到引用的临时类对象 obj 进行复制初始化时,可以通过将 obj 直接构造到 target 中来省略复制初始化。这种副本消除的变体称为 未命名返回值优化 (URVO)。自 C++17 起,URVO 是强制性的,不再被视为副本消除的一种形式;详见下文。
(C++17 前)
  • throw 表达式中,当操作数是非 volatile 且具有自动存储期的对象 obj 的名称时(函数参数或处理函数参数除外),该对象属于不包含最内层 try(如果存在)的 作用域,可以通过将 obj 直接构造到异常对象中来省略异常对象的复制初始化。
  • 处理函数中,如果程序的含义不变,除了处理函数参数的构造函数和析构函数的执行,可以通过将处理函数参数视为异常对象的别名来省略处理函数参数的复制初始化。
(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 对象被销毁,返回类型的析构函数也必须在 return 语句处可访问且未被删除。
  • 在对象初始化中,当初始化表达式是与变量类型相同的类类型(忽略 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++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 只能消除复制(/移动)操作,但
复制初始化可以选择非复制(/移动)构造函数
消除任何对象构造
相关复制初始化

[编辑] 另见