类复制省略 (copy elision)
当满足特定条件时,即使所选对象的构造函数和/或析构函数有副作用,实现也被允许省略从相同类型(忽略 cv 限定符)的源对象创建类对象。
在这种情况下,实现将省略的初始化的源和目标视为引用同一对象的两种不同方式 。如果所选构造函数的第一个参数是对对象类型的右值引用,则该对象的析构发生在目标对象本应被析构的时刻;否则,析构发生在未进行优化时两个对象本应被析构的较晚时刻。
[注 1]: 因为只有一个对象被析构而不是两个,并且一个对象的创建被省略了,所以对于每个构造的对象仍然只有一个对象被析构。------尾注
这种对象创建的省略,称为复制省略 (copy elision),在以下情况下是允许的(这些情况可以组合以消除多次复制):
-
在返回语句中 (return statement): 在具有类返回类型的函数中,当表达式是具有自动存储期的非 volatile 对象
o
(不是函数参数,也不是由处理程序的异常声明引入的变量)的名称时,可以通过直接将o
构造到函数调用的结果对象中来省略结果对象的复制初始化。 -
在 throw 表达式中 (throw-expression): 在 throw 表达式中,当操作数是具有自动存储期的非 volatile 对象
o
(不是函数参数,也不是由处理程序的异常声明引入的变量)的名称,且该对象所属的作用域不包含与 try 块关联的最内层复合语句(如果存在)时,可以通过直接将o
构造到异常对象中来省略异常对象的复制初始化。 -
在协程中 (coroutine): 在协程中,如果程序的语义除了执行参数拷贝对象的构造函数和析构函数之外保持不变,则可以省略协程参数的拷贝,并将对该拷贝的引用替换为对相应参数的引用。
-
在异常处理器的异常声明中:当处理程序的异常声明声明了一个对象
o
时,如果程序的语义除了执行异常声明所声明的对象的构造函数和析构函数之外保持不变,则可以通过将异常声明视为异常对象的别名来省略o
的复制初始化。[注 2]: 不能从异常对象进行移动,因为它总是左值。------尾注
在要求常量表达式的上下文中计算表达式时,以及在常量初始化时,不允许进行复制省略。
[注 3]: 如果同一表达式在另一个上下文中求值,仍有可能执行复制省略。------尾注
[示例 1]:
cpp
class Thing {
public:
Thing();
~Thing();
Thing(const Thing&);
};
Thing f() {
Thing t;
return t; // 允许省略将 t 复制/移动到 f() 的结果对象
}
Thing t2 = f(); // 允许省略将 f() 的返回值复制/移动到 t2
struct A {
void* p;
constexpr A() : p(this) {}
};
constexpr A g() {
A loc;
return loc; // 常量求值上下文: 不允许省略!
}
constexpr A a; // 正确,a.p 指向 a
constexpr A b = g(); // 错误: b.p 将是悬垂指针 ([expr.const])
void h() {
A c = g(); // 正确, c.p 可以指向 c 或是悬垂的
}
此例中,省略标准可以消除将具有自动存储期的对象
t
复制到函数调用f()
的结果对象(即非局部对象t2
)中。实际上,t
的构造可以被视为直接初始化t2
,并且该对象的析构将发生在程序退出时。向Thing
添加移动构造函数具有相同的效果,但被省略的是从具有自动存储期的对象到t2
的移动构造。------尾例
[示例 2]:
cpp
class Thing {
public:
Thing();
~Thing();
Thing(Thing&&);
private:
Thing(const Thing&); // 复制构造函数私有,强制使用移动(如果可能)
};
Thing f(bool b) {
Thing t;
if (b)
throw t; // 正确, 使用 Thing(Thing&&) (或省略) 来抛出 t
return t; // 正确, 使用 Thing(Thing&&) (或省略) 来返回 t
}
Thing t2 = f(false); // 正确, 没有额外的复制/移动操作, t2 由对 f 的调用构造
struct Weird {
Weird();
Weird(Weird&); // 注意:非常量左值引用复制构造函数
};
Weird g(bool b) {
static Weird w1; // 静态存储期
Weird w2; // 自动存储期
if (b)
return w1; // 正确, 使用 Weird(Weird&) (w1 是左值)
else
return w2; // 错误: 在此上下文中 w2 是 xvalue (将亡值),但 Weird 没有接受右值的构造函数
}
int& h(bool b, int i) {
static int s;
if (b)
return s; // 正确,返回静态变量的左值引用
else
return i; // 错误: i 是自动变量,在此上下文中是 xvalue,但函数返回左值引用
}
decltype(auto) h2(Thing t) {
return t; // 正确, t 是 xvalue, h2 的返回类型推导为 Thing (值类型)
}
decltype(auto) h3(Thing t) {
return (t); // 正确, (t) 是 xvalue, h3 的返回类型推导为 Thing&& (右值引用)
}
------尾例
[示例 3]:
cpp
template <class T> void g(const T&);
template <class T> void f() {
T x; // 外层作用域对象
try {
T y; // 内层作用域对象
try {
g(x);
} catch (...) {
if (/*...*/)
throw x; // 不会移动 (x 属于包含 try 块的作用域,不符合 1.2 省略条件)
throw y; // 移动 (y 属于不包含 try 块的内层作用域,符合 1.2 省略条件。若省略则直接构造异常对象)
}
g(y);
} catch (...) {
g(x);
g(y); // 错误: y 不在作用域内
}
}
------尾例
核心总结:
此标准条款定义了 C++ 中的复制/移动省略 (Copy/Move Elision) 规则,这是编译器为了优化性能而避免不必要的对象复制或移动的关键机制。
- 本质与效果:
- 允许编译器在特定条件下,完全省略从一个对象(源)创建另一个同类型对象(目标)的操作。
- 被省略后,源和目标被视为同一个对象的两种引用方式。
- 析构时机:若使用移动构造函数,则在目标对象该析构时析构;若使用复制构造函数,则在源和目标原该析构的较晚时刻析构。最终效果是只构造和析构了一个对象。
- 允许省略的场景 (可组合使用):
- 命名返回值优化 (NRVO): 函数返回局部非 volatile 自动存储期对象(非参数/异常声明变量)的名称时 (
return local_var;
),可省略将local_var
复制/移动到函数返回值的操作,直接在返回值位置构造。 - Throw 表达式优化: 抛出局部非 volatile 自动存储期对象(非参数/异常声明变量,且其作用域不包含最内层 try 块)的名称时,可省略将其复制/移动到异常对象的操作,直接在异常对象位置构造。
- 协程参数省略: 在协程中,可省略对协程参数的拷贝。
- 异常处理器别名: 在
catch
块的异常声明中 (catch (Type obj)
),可省略对异常对象的拷贝,直接将obj
视为异常对象的别名。
- 命名返回值优化 (NRVO): 函数返回局部非 volatile 自动存储期对象(非参数/异常声明变量)的名称时 (
- 禁止省略的场景:
- 常量表达式求值: 在要求常量表达式的上下文中(如
constexpr
变量初始化、constexpr
函数内的 return)。 - 常量初始化 (静态初始化): 在静态存储期对象的常量初始化过程中。
- 常量表达式求值: 在要求常量表达式的上下文中(如
- 关键点与影响:
- 性能提升: 省略操作避免了潜在的昂贵复制/移动构造函数和析构函数调用,显著提升性能。
- 副作用容忍: 即使被省略的构造函数或析构函数有可观测的副作用(如打印日志),编译器仍被允许进行省略(这是
as-if
规则的例外)。 - 移动构造的特殊性: 条款明确说明了当省略涉及移动构造函数时析构发生的时机。
- 标准要求 (C++17起): 对于 NRVO (场景 1) 和纯右值初始化,满足条件时编译器必须进行省略(称为"强制省略"或"保证的复制省略")。其他场景(如 throw 优化)是允许但不强制的。
- 作用域与生存期: 示例 2 和 3 展示了对象的作用域(特别是相对于
try
块的位置)如何影响省略的可行性,以及在异常处理中对象生存期的微妙问题。
简而言之: 此条款赋予编译器权力,在特定且定义明确的场景下(尤其是函数返回局部对象和抛出局部对象时),可以完全绕过复制或移动构造函数,直接"复用"源对象作为目标对象,从而生成更高效的代码。理解这些规则对于编写高性能 C++ 代码和避免不必要的 std::move
(如之前文章所述)至关重要。
原文翻译
11.9 Initialization
11.9 初始化类.init
11.9.6 Copy/move elision
11.9.6 复制/移动省略[class.copy.elision] [类复制.省略]
When certain criteria are met, an implementation is allowed to omit the creation of a class object from a source object of the same type (ignoring cv-qualification), even if the selected constructor and/or the destructor for the object have side effects.
当满足某些条件时,实现为 允许省略 Class Object 的创建 相同类型的源对象(忽略 cv 限定), 即使所选构造函数和/或 对象的析构函数具有 副作用 。
In such cases, the implementation treats the source and target of the omitted initialization as simply two different ways of referring to the same object.
在这种情况下,实现将省略的初始化的 source 和 target 视为引用同一对象的两种不同方式 。
If the first parameter of the selected constructor is an rvalue reference to the object's type, the destruction of that object occurs when the target would have been destroyed; otherwise, the destruction occurs at the later of the times when the two objects would have been destroyed without the optimization.
如果所选构造函数的第一个参数是对象类型的右值引用,则当目标已销毁时,将销毁该对象;否则,销毁发生在没有优化的情况下销毁两个对象的时间 。
*Note [1](https://eel.is/c++draft/class.copy.elision#note-1)*: Because only one object is destroyed instead of two, and the creation of one object is omitted, there is still one object destroyed for each one constructed[.](https://eel.is/c++draft/class.copy.elision#1.sentence-4) --- *end note*
*注 [1](https://eel.is/c++draft/class.copy.elision#note-1)*: 因为只有一个对象被销毁而不是两个,并且省略了一个对象的创建,所以每个构建的对象仍然有一个对象被销毁 [。](https://eel.is/c++draft/class.copy.elision#1.sentence-4)
这种对象创建的省略称为 复制省略 / 允许在 以下情况(可能会与 消除多个拷贝):
-
in a return statement ([stmt.return]) in a function with a class return type, when the expression is the name of a non-volatile object o with automatic storage duration (other than a function parameter or a variable introduced by the exception-declaration of a handler ([except.handle])), the copy-initialization of the result object can be omitted by constructing o directly into the function call's result object;
在具有类 return 类型的函数的 return 语句 ([stmt.return]) 中,当表达式是具有自动存储持续时间的非易失性对象的名称 o 时(函数参数或由 a 的异常声明引入的变量除外) handler ([除外。handle])),可以通过将 o 直接构造到函数调用的 result 对象中来省略 result 对象的复制初始化;
-
in a throw-expression ([expr.throw]), when the operand is the name of a non-volatile object o with automatic storage duration (other than a function parameter or a variable introduced by the exception-declaration of a handler) that belongs to a scope that does not contain the innermost enclosing compound-statement associated with a try-block (if there is one), the copy-initialization of the exception object can be omitted by constructing o directly into the exception object;
在 throw 表达式 ([expr.throw]),当作数是具有自动存储持续时间的非易失性对象 o 的名称(函数参数或处理程序的异常声明引入的变量除外)时,该对象属于不包含最内层封闭复合语句的作用域 与 try 块 (如果有)相关联,可以通过将 o 直接构造到 Exception 对象中来省略 Exception 对象的复制初始化;
-
in a coroutine, a copy of a coroutine parameter can be omitted and references to that copy replaced with references to the corresponding parameter if the meaning of the program will be unchanged except for the execution of a constructor and destructor for the parameter copy object;
在协程中,如果程序的含义保持不变,则除了执行参数 copy 对象的构造函数和析构函数外,可以省略协程参数的副本,并将对该副本的引用替换为对相应参数的引用;
-
when the exception-declaration of a handler ([except.handle]) declares an object o, the copy-initialization of o can be omitted by treating the exception-declaration as an alias for the exception object if the meaning of the program will be unchanged except for the execution of constructors and destructors for the object declared by the exception-declaration.
当处理程序 ([except.handle]) 的异常声明声明对象 o 时,如果程序的含义保持不变,则可以通过将异常声明视为异常对象的别名来省略 o 的复制初始化,但异常声明声明的对象除外。
*Note [2](https://eel.is/c++draft/class.copy.elision#note-2)*: There cannot be a move from the exception object because it is always an lvalue[.](https://eel.is/c++draft/class.copy.elision#1.4.sentence-2) --- *end note*
*注 [2](https://eel.is/c++draft/class.copy.elision#note-2)*: 不能从异常对象移动,因为它始终是左值 [。](https://eel.is/c++draft/class.copy.elision#1.4.sentence-2)
*Note [3](https://eel.is/c++draft/class.copy.elision#note-3)*: It is possible that copy elision is performed if the same expression is evaluated in another context[.](https://eel.is/c++draft/class.copy.elision#1.sentence-7) --- *end note*
*注 [3](https://eel.is/c++draft/class.copy.elision#note-3)*: 如果在另一个上下文中计算相同的表达式,则可能会执行复制省略 [。](https://eel.is/c++draft/class.copy.elision#1.sentence-7)
*Example [1](https://eel.is/c++draft/class.copy.elision#example-1)*: ```cpp class Thing { public: Thing(); ~Thing(); Thing(const Thing&); }; Thing f() { Thing t; return t; // 允许省略将 t 复制/移动到 f() 的结果对象 } Thing t2 = f(); // 允许省略将 f() 的返回值复制/移动到 t2 struct A { void* p; constexpr A() : p(this) {} }; constexpr A g() { A loc; return loc; // 常量求值上下文: 不允许省略! } constexpr A a; // well-formed, a.p points to a ;正确,a.p 指向 a constexpr A b = g(); // error: b.p would be dangling ([expr.const]);错误: b.p 将是悬垂指针 ([expr.const]) void h() { A c = g(); // well-formed, c.p can point to c or be dangling;正确, c.p 可以指向 c 或是悬垂的 } ``` --- *end example*
这里的省略标准可以消除将具有自动存储持续时间的对象 t 复制到函数调用 f() 的结果对象中,即非本地对象 t2。
实际上,t 的构造可以看作是直接初始化 t2,并且该对象的销毁将在程序退出时发生 。
向 Thing 添加移动构造函数具有相同的效果,但省略了从具有自动存储持续时间的对象到 t2 的移动构造 。
*Example [2](https://eel.is/c++draft/class.copy.elision#example-2)*: ```cpp class Thing { public: Thing(); ~Thing(); Thing(Thing&&); private: Thing(const Thing&); // 复制构造函数私有,强制使用移动(如果可能) }; Thing f(bool b) { Thing t; if (b) throw t; //OK, Thing(Thing&&) used (or elided) to throw t; 正确, 使用 Thing(Thing&&) (或省略) 来抛出 t return t; // OK, Thing(Thing&&) used (or elided) to return t;正确, 使用 Thing(Thing&&) (或省略) 来返回 t } Thing t2 = f(false); // OK, no extra copy/move performed, t2 constructed by call to f ;正确, 没有额外的复制/移动操作, t2 由对 f 的调用构造 struct Weird { Weird(); Weird(Weird&); // 注意:非常量左值引用复制构造函数 }; Weird g(bool b) { static Weird w1; // 静态存储期 Weird w2; // 自动存储期 if (b) return w1; // OK, uses Weird(Weird&);正确, 使用 Weird(Weird&) (w1 是左值) else return w2; // error: w2 in this context is an xvalue;错误: 在此上下文中 w2 是 xvalue (将亡值),但 Weird 没有接受右值的构造函数 } int& h(bool b, int i) { static int s; if (b) return s; // OK;正确,返回静态变量的左值引用 else return i; // error: i is an xvalue;错误: i 是自动变量,在此上下文中是 xvalue,但函数返回左值引用 } decltype(auto) h2(Thing t) { return t; // OK, t is an xvalue and h2's return type is Thing;正确, t 是 xvalue, h2 的返回类型推导为 Thing (值类型) } decltype(auto) h3(Thing t) { return (t); // OK, (t) is an xvalue and h3's return type is Thing&&;正确, (t) 是 xvalue, h3 的返回类型推导为 Thing&& (右值引用) } ``` --- *end example*
*Example [3](https://eel.is/c++draft/class.copy.elision#example-3)*:
```cpp
template