Namespaces
Variants

Copy elision

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

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

目录

说明

在以下情况下允许进行复制消除(这些情况可以组合使用以消除多次复制):

  • 在具有类返回类型的函数中的 return 语句 中,当操作数是具有 自动存储期 的非易失性对象(除函数参数或 异常处理程序 参数外)的名称 obj 时,可以通过将 obj 直接构造到函数调用的结果对象中来省略结果对象的 复制初始化 。这种复制消除的变体称为 命名返回值优化 (NRVO)。
  • 当类对象 target 通过未被绑定到引用的临时类对象 obj 进行复制初始化时,可以通过将 obj 直接构造到 target 中来省略复制初始化。这种复制消除的变体称为 无名返回值优化 (URVO)。自 C++17 起,URVO 成为强制性要求,不再被视为复制消除的一种形式;详见下文。
(C++17 前)
  • throw 表达式 中,当操作数是具有自动存储期的非易失对象 obj (非函数参数或处理程序参数)的名称,且该对象所属的 作用域 不包含最内层封闭的 try (如果存在)时,可以通过将 obj 直接构造到异常对象中来省略异常对象的复制初始化。
  • 处理程序 中,如果程序的语义在处理程序参数的构造函数和析构函数执行之外保持不变,则可以通过将处理程序参数视为异常对象的别名来省略处理程序参数的复制初始化。
(C++11 起)
  • 协程 中,可以省略协程参数的副本。在这种情况下,如果程序的语义在参数副本对象的构造函数和析构函数执行之外保持不变,则对该副本的引用将被替换为对相应参数的引用。
(C++20 起)

当发生复制消除时,编译器将被省略初始化的源对象和目标对象视为引用同一对象的两种不同方式。

在不进行优化的情况下,析构发生在两个对象原本会被销毁的时间中较晚的时刻。

(until C++11)

若所选构造函数的首个参数是该对象类型的右值引用,则该对象的析构发生在目标对象原本会被销毁的时刻。否则,析构发生在两个对象原本会被销毁的时间中较晚的时刻。

(since 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 对象被销毁。
  • 在对象初始化时,当初始化表达式是与变量类型相同类类型(忽略 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 机制的另一种方式是"非具体化值传递"或"延迟临时对象具体化":纯右值在从未具体化临时对象的情况下被返回和使用。

(since 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
(since 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(); // (C++17前) 从临时对象初始化v时的复制消除;
                       //               可能调用移动构造函数
                       // (C++17起) "保证复制消除"
    return v; // 从v到结果对象的复制消除("NRVO");
              // 可能调用移动构造函数
}
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()返回值初始化arg时的复制消除
            // (C++17起) "保证复制消除"
}

可能的输出:

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 仅能消除复制/移动操作,但复制初始化
可能选择非复制/移动构造函数
消除相关复制初始化中
的所有对象构造

参阅