什么是复制省略?什么是(命名的)返回值优化?它们意味着什么?
对于技术概述 复制省略在标准中定义:
作为
\31) 当满足某些条件时,允许实现省略类对象的复制/移动构造,即使对象的复制/移动构造函数和/或析构函数有副作用。在这种情况下,实现将省略的复制/移动操作的源和目标简单地视为引用同一对象的两种不同方式,并且该对象的销毁发生在两个对象本应被删除的较晚时间。没有优化就被破坏了。123这种复制/移动操作的省略,称为复制省略,在以下情况下是允许的(可以结合起来消除多个副本): — 在具有类返回类型的函数的 return 语句中,当表达式是具有与函数返回类型相同的 cvunqualified 类型的非易失性自动对象(函数或 catch 子句参数除外)的名称时,可以通过将自动对象直接构造到函数的返回值中来省略复制/移动操作 — 在 throw 表达式中,当操作数是非易失性自动对象(函数或 catch 子句参数除外)的名称时,其范围不超出最里面的封闭 try 块的末尾(如果有一)、从操作数到异常对象(15.1)的复制/移动操作可以通过将自动对象直接构造到异常对象中来省略 — 当尚未绑定到引用 (12.2) 的临时类对象将被复制/移动到具有相同 cv-unqualified 类型的类对象时,可以通过将临时对象直接构造到省略复制/移动的目标 — 当异常处理程序的异常声明(第 15 条)声明与异常对象(15.1)具有相同类型的对象(cv 限定除外)时,可以通过处理异常声明来省略复制/移动操作作为异常对象的别名,如果程序的含义将保持不变,除了为异常声明声明的对象执行构造函数和析构函数。 \123) 因为只有一个对象被销毁而不是两个,并且没有执行一个复制/移动构造函数,所以每构造一个对象,仍然有一个对象被销毁。
\31) 当满足某些条件时,允许实现省略类对象的复制/移动构造,即使对象的复制/移动构造函数和/或析构函数有副作用。在这种情况下,实现将省略的复制/移动操作的源和目标简单地视为引用同一对象的两种不同方式,并且该对象的销毁发生在两个对象本应被删除的较晚时间。没有优化就被破坏了。123这种复制/移动操作的省略,称为复制省略,在以下情况下是允许的(可以结合起来消除多个副本):
— 在具有类返回类型的函数的 return 语句中,当表达式是具有与函数返回类型相同的 cvunqualified 类型的非易失性自动对象(函数或 catch 子句参数除外)的名称时,可以通过将自动对象直接构造到函数的返回值中来省略复制/移动操作
— 在 throw 表达式中,当操作数是非易失性自动对象(函数或 catch 子句参数除外)的名称时,其范围不超出最里面的封闭 try 块的末尾(如果有一)、从操作数到异常对象(15.1)的复制/移动操作可以通过将自动对象直接构造到异常对象中来省略
— 当尚未绑定到引用 (12.2) 的临时类对象将被复制/移动到具有相同 cv-unqualified 类型的类对象时,可以通过将临时对象直接构造到省略复制/移动的目标
— 当异常处理程序的异常声明(第 15 条)声明与异常对象(15.1)具有相同类型的对象(cv 限定除外)时,可以通过处理异常声明来省略复制/移动操作作为异常对象的别名,如果程序的含义将保持不变,除了为异常声明声明的对象执行构造函数和析构函数。
\123) 因为只有一个对象被销毁而不是两个,并且没有执行一个复制/移动构造函数,所以每构造一个对象,仍然有一个对象被销毁。
给出的例子是:
class Thing { public: Thing(); ~Thing(); Thing(const Thing&); }; Thing f() { Thing t; return t; } Thing t2 = f();
并解释说:
这里可以结合省略的标准来消除对类的复制构造函数的两次调用Thing:将本地自动对象复制t到临时对象中以获取函数的返回值,f() 以及将临时对象复制到对象t2中。实际上,本地对象的构造t 可以看作是直接初始化全局对象t2,而该对象的销毁将在程序退出时发生。将移动构造函数添加到 Thing 具有相同的效果,但它是从临时对象到的移动构造t2被省略了。
Thing
t
f()
t2
对于发生复制省略的常见情况 (命名)返回值优化是复制省略的一种常见形式。它指的是从方法中通过值返回的对象的副本被删除的情况。标准中提出的示例说明了命名返回值优化,因为对象是命名的。
返回临时值时会发生常规返回值优化:
class Thing { public: Thing(); ~Thing(); Thing(const Thing&); }; Thing f() { return Thing(); } Thing t2 = f();
发生复制省略的其他常见位置是通过值传递临时值时:
class Thing { public: Thing(); ~Thing(); Thing(const Thing&); }; void foo(Thing t); foo(Thing());
或者当异常被抛出并被 value 捕获时:
struct Thing{ Thing(); Thing(const Thing&); }; void foo() { Thing c; throw c; } int main() { try { foo(); } catch(Thing c) { } }
复制省略的常见限制是:
大多数商业级编译器支持复制省略和 (N)RVO(取决于优化设置)。
复制省略是大多数编译器实现的一种优化,用于在某些情况下防止额外的(可能是昂贵的)复制。它使按值返回或按值传递在实践中可行(有限制)。
这是省略(哈!)as-if 规则的唯一优化形式 - 即使复制/移动对象有副作用,也可以应用复制省略 。
以下示例取自维基百科:
struct C { C() {} C(const C&) { std::cout << "A copy was made.\n"; } }; C f() { return C(); } int main() { std::cout << "Hello World!\n"; C obj = f(); }
根据编译器和设置,以下输出 都是有效 的:
你好世界! 制作了一份副本。 制作了一份副本。
你好世界! 制作了一份副本。
你好世界!
这也意味着可以创建更少的对象,因此您也不能依赖于调用特定数量的析构函数。您不应该在复制/移动构造函数或析构函数中包含关键逻辑,因为您不能依赖它们被调用。
如果省略了对复制或移动构造函数的调用,则该构造函数必须仍然存在并且必须是可访问的。这确保复制省略不允许复制通常不可复制的对象,例如因为它们具有私有或已删除的复制/移动构造函数。
**C17** :从 C17 开始,直接返回对象时保证复制省略:
struct C { C() {} C(const C&) { std::cout << "A copy was made.\n"; } }; C f() { return C(); //Definitely performs copy elision } C g() { C c; return c; //Maybe performs copy elision } int main() { std::cout << "Hello World!\n"; C obj = f(); //Copy constructor isn't called }