复制消除

来自cppreference.com
< cpp‎ | language


 
 
C++ 语言
 
 

省略复制及移动 (C++11 起)构造函数,导致零复制的按值传递语义。

解释

纯右值语义(“有保证的复制消除”)

从 C++17 起,非必须不会将纯右值实质化,并且它会被直接构造到其最终目标的存储中。这有时候意味着,即便语言的语法看起来进行了复制/移动(例如复制初始化),也并不进行复制/移动——这表示该类型完全不需要具有可访问的复制/移动构造函数。其例子包括:

T f()
{
    return U(); // 构造一个 U 类型的临时量,然后从临时量初始化返回的 T
}
T g()
{
    return T(); // 直接构造返回的 T;没有移动
}
返回类型的析构函数必须在 return 语句位置可访问且未被弃置,即使没有 T 对象要被销毁也是如此。
  • 在对象的初始化中,当初始化器表达式是一个与变量类型相同(忽略 cv 限定)的类类型的纯右值时:
T x = T(T(f())); // 直接以 f() 的结果初始化 x;没有移动
只能在已知要初始化的对象不是潜在重叠的子对象时应用此规则:
struct C { /* ... */ };
C f();
 
struct D;
D g();
 
struct D : C
{
    D() : C(f()) {}    // 初始化基类子对象时无消除
    D(int) : D(g()) {} // 无消除,因为正在初始化的 D 对象可能是某个其他类的基类子对象
};

注意:上述规则指定的不是优化,并且标准并未正式将其描述为“复制消除”(因为并无被消除的东西)。针对纯右值临时量的 C++17 核心语言规定在本质上不同于之前的 C++ 版本:不再有用于复制/移动的临时量。描述 C++17 机制的另一种方式是“未实质化的值传递”或“延迟临时量实质化”:返回并使用纯右值时不实质化临时量。

(C++17 起)

非强制的复制/移动 (C++11 起)操作消除

下列情形下,允许但不要求编译器省略类对象的复制和移动 (C++11 起)构造,即使复制/移动 (C++11 起)构造函数和析构函数拥有可观察的副作用。这些对象将直接构造到它们本来要复制/移动到的存储中。这是一项优化:即使进行了优化而不调用复制/移动 (C++11 起)构造函数,它仍然必须存在且可访问(如同完全未发生优化),否则程序非良构:

  • return 语句中,当操作数是拥有自动存储期的非 volatile 对象的名字,该名字不是函数形参或处理块形参,且其具有与函数返回类型相同(忽略 cv 限定)的类类型时。这种复制消除的变体被称为 NRVO,“具名返回值优化”。
  • 在对象的初始化中,当源对象是无名临时量且与目标对象具有相同类型(忽略 cv 限定)时。当无名临时量为 return 语句的操作数时,称这种复制消除的变体为 URVO,“无名返回值优化”。(从 C++17 开始,无名返回值优化是强制要求的,而不再被当做复制消除;见上文。)
(C++17 前)
  • throw 表达式中,当操作数是具有自动存储期的非 volatile 对象的名字,它不是函数形参或处理块形参,且它的作用域不延伸超过最内层的 try(如果存在)时。
  • 处理块中,当实参与抛出的异常对象具有相同类型(忽略 cv 限定)时,省略异常对象的复制,而该处理块体直接访问该异常对象,如同按引用捕获它一样(不可能从异常对象移动,因为它始终是左值)。如果这种复制消除会因为除了跳过该处理块形参的复制构造函数和析构函数之外的任何原因,导致程序的可观察行为发生改变,那么它被禁止(例如,当修改了处理块的实参,并以 throw 重新抛出异常对象时)。
(C++11 起)
  • 协程中,可以消除将形参向协程状态内的复制/移动,只要除了对形参的构造函数与析构函数的调用被忽略以外,不改变程序的行为即可。这种情形可以在暂停点后始终不使用形参,或者整个协程状态本就始终不在堆上分配时出现。
(C++20 起)

进行复制消除时,实现将被省略的复制/移动 (C++11 起)操作的源和目标单纯地当做指代同一对象的两种不同方式,而该对象将在假如不进行优化时两个对象中后被销毁的对象销毁时销毁(但如果被选择的构造函数的形参是对象类型的右值引用,那么该销毁发生于目标对象本应被销毁时) (C++11 起)

可以连锁多次复制消除,以消除多次复制。

struct A
{
    void* p;
    constexpr A() : p(this) {}
    A(const A&); // 禁用可平凡复制性
};
 
constexpr A a;  // OK: a.p 指向 a
 
constexpr A f()
{
    A x;
    return x;
}
constexpr A b = f(); // 错误:b.p 会悬垂,并会指向 f 中的 x
 
constexpr A c = A(); // (C++17 前) error: c.p 将悬垂并指向临时量
                     // (C++17 起) OK: c.p 指向 c; 不涉及临时量
(C++11 起)

注解

复制消除是允许改变可观察副作用的唯一得到允许的优化形式 (C++14 前)两种允许的优化形式之一,另一种是分配消除与扩展 (C++14 起)。因为一些编译器并不在所有允许的场合中进行复制消除(例如调试模式下),依赖于复制/移动构造函数和析构函数的副作用的程序是不可移植的。

return 语句或 throw 表达式中,如果编译器不能进行复制消除,但满足或者(若非源是函数形参)本应满足复制消除的条件,那么即使源操作数由左值代表,编译器也将尝试使用移动构造函数 (C++23 前)就会将源操作数当做右值 (C++23 起);细节见 return 语句

(C++11 起)
功能特性测试宏 标准 功能特性
__cpp_guaranteed_copy_elision 201606L (C++17) 通过简化的值类别提供有保证的复制消除

示例

#include <iostream>
 
struct Noisy
{
    Noisy() { std::cout << "在 " << this << " 构造" << '\n'; }
    Noisy(const Noisy&) { std::cout << "复制构造\n"; }
    Noisy(Noisy&&) { std::cout << "移动构造\n"; }
    ~Noisy() { std::cout << "在 " << this << " 析构" << '\n'; }
};
 
Noisy f()
{
    Noisy v = Noisy(); // (C++17 前) 从临时量初始化 v 时发生复制消除,可能调用移动构造函数
                       // (C++17 起) "有保证的复制消除"
    return v; // 从 v 到结果对象的复制消除,可能调用移动构造函数
}
 
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() 的结果初始化实参时发生复制消除
            // (C++17 起) "有保证的复制消除"
}

可能的输出:

在 0x7fffd635fd4e 构造
&v = 0x7fffd635fd4e
在 0x7fffd635fd4f 构造
&arg = 0x7fffd635fd4f
在 0x7fffd635fd4f 析构
在 0x7fffd635fd4e 析构

缺陷报告

下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。

缺陷报告 应用于 出版时的行为 正确行为
CWG 1967 C++11 在通过移动构造函数完成复制消除时依然会考虑被移动的对象的生存期 不考虑
CWG 2022 C++11 常量求值中复制消除曾是可选的 常量求值中强制复制消除
CWG 2278 C++11 常量求值中曾强制复制消除 常量求值中禁止复制消除
CWG 2426 C++17 返回纯右值时不要求析构函数 潜在调用析构函数

参阅