什么是复制elision和返回值优化?

什么是复制elision? 什么是(命名)返回值优化? 他们意味着什么?

在什么情况下可以发生? 什么是限制?

  • 如果你是参考这个问题,你可能正在寻找介绍
  • 有关技术概述,请参阅标准参考
  • 在这里查看常见情况

介绍

技术概述 – 跳到这个答案 。

对于出现复制瑕疵的常见情况,请跳至此答案 。

复制elision是由大多数编译器实现的优化,以防止在某些情况下额外(可能是昂贵的)副本。 它使价值回归或价值传递在实践中可行(限制适用)。

这是唯一的优化forms, 即使复制/移动对象具有副作用,elide (ha!)as-if规则 – 复制elision也可以应用

下面的例子来自维基百科 :

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(); } 

根据编译器和设置,以下输出全部有效

你好,世界!
复制了。
复制了。


你好,世界!
复制了。


你好,世界!

这也意味着可以创build更less的对象,所以你也不能依赖被调用的特定数量的析构函数。 你不应该在复制/移动构造函数或析构函数中有关键的逻辑,因为你不能依靠它们被调用。

如果消除对副本或移动构造函数的调用,则该构造函数必须仍然存在,并且必须是可访问的。 这确保了copy elision不允许复制通常不可复制的对象,例如因为它们具有私有的或删除的复制/移动构造器。

标准参考

对于较less的技术观点和介绍 – 跳到这个答案 。

对于出现复制瑕疵的常见情况,请跳至此答案 。

复制elision是在标准中定义的:

12.8复制和移动类对象[class.copy]

31)当满足某些标准时,即使对象的复制/移动构造函数和/或析构函数具有副作用,也允许实现省略类对象的复制/移动构造。 在这种情况下,实现将被忽略的复制/移动操作的来源和目标作为简单地引用同一对象的两种不同的方式来处理,并且该对象的破坏发生在两个对象已经被没有优化就销毁了。 123在下列情况下,允许复制/移动操作的缩写 (称为复制删除)(可以合并以消除多个副本):

– 在具有类返回types的函数的返回语句中,当expression式是与函数返回types具有相同cvun限定types的非易失性自动对象(函数或catch-clause参数除外)的名称时,可以通过将自动对象直接构造到函数的返回值中来省略复制/移动操作

– 在throwexpression式中,当操作数是非范围的自动对象(函数或catch-clause参数除外)的作用域的范围不超出最内层的try-block的末尾时(如果存在一个),通过将自动对象直接构造到exception对象中,可以省略从操作数到exception对象(15.1)的复制/移动操作

– 当未绑定到引用(12.2)的临时类对象将被复制/移动到具有相同cv-unqualifiedtypes的类对象时,可以通过将临时对象直接构造到省略复制/移动的目标

– 当exception处理程序(第15章)的exception声明声明与exception对象(15.1)相同types的对象(cv-qualification除外)时,复制/移动操作可以通过处理exception声明作为exception对象的别名,除非由exception声明声明的对象的构造函数和析构函数执行,否则程序的含义将保持不变。

123)因为只有一个对象被破坏而不是两个,并且一个复制/移动构造函数没有被执行,所以每个构造对象仍然有一个被销毁。

给出的例子是:

 class Thing { public: Thing(); ~Thing(); Thing(const Thing&); }; Thing f() { Thing t; return t; } Thing t2 = f(); 

并解释说:

这里elision的标准可以被合并,以消除对Thing类的拷贝构造函数的两个调用:将本地自动对象t复制到函数f()的返回值的临时对象中,并将该临时对象复制到对象t2 。 实际上,本地对象t的构造可以被看作是直接初始化全局对象t2 ,并且该对象的销毁将在程序出口处发生。 给Thing添加一个移动构造函数也有同样的效果,但是它是从临时对象到t2被移除的移动构造。

常见的复制forms

技术概述 – 跳到这个答案 。

对于较less的技术观点和介绍 – 跳到这个答案 。

(命名)返回值优化是复制elision的常见forms。 它指的是从方法返回的对象的副本消失的情况。 标准中提出的例子说明了命名的返回值优化 ,因为该对象被命名。

 class Thing { public: Thing(); ~Thing(); Thing(const Thing&); }; Thing f() { Thing t; return t; } Thing t2 = f(); 

定期返回值优化发生在临时返回时:

 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()); 

或者当一个exception被抛出并被值捕获

 struct Thing{ Thing(); Thing(const Thing&); }; void foo() { Thing c; throw c; } int main() { try { foo(); } catch(Thing c) { } } 

复制elision的常见限制是:

  • 多个返回点
  • 条件初始化

大多数商业级编译器都支持copy elision和(N)RVO(取决于优化设置)。

复制elision是一种编译器优化技术,可以消除不必要的对象复制/移动。

在以下情况下,编译器可以省略复制/移动操作,因此不会调用相关的构造函数:

  1. NRVO(命名返回值优化) :如果一个函数按值返回一个类的types,并且返回语句的expression式是具有自动存储持续时间(不是函数参数)的非易失性对象的名称,则复制/移动可以省略由非优化编译器执行的操作。 如果是这样,则返回值直接在存储中构造,否则函数的返回值将被移动或复制到该存储器中。
  2. RVO(返回值优化) :如果函数返回一个无名的临时对象,将被一个朴素的编译器移动或复制到目的地,那么可以按照1省略复制或移动。
 #include <iostream> using namespace std; class ABC { public: const char *a; ABC() { cout<<"Constructor"<<endl; } ABC(const char *ptr) { cout<<"Constructor"<<endl; } ABC(ABC &obj) { cout<<"copy constructor"<<endl;} ABC(ABC&& obj) { cout<<"Move constructor"<<endl; } ~ABC() { cout<<"Destructor"<<endl; } }; ABC fun123() { ABC obj; return obj; } ABC xyz123() { return ABC(); } int main() { ABC abc; ABC obj1(fun123());//NRVO ABC obj2(xyz123());//NRVO ABC xyz = "Stack Overflow";//RVO return 0; } **Output without -fno-elide-constructors** root@ajay-PC:/home/ajay/c++# ./a.out Constructor Constructor Constructor Constructor Destructor Destructor Destructor Destructor **Output with -fno-elide-constructors** root@ajay-PC:/home/ajay/c++# g++ -std=c++11 copy_elision.cpp -fno-elide-constructors root@ajay-PC:/home/ajay/c++# ./a.out Constructor Constructor Move constructor Destructor Move constructor Destructor Constructor Move constructor Destructor Move constructor Destructor Constructor Move constructor Destructor Destructor Destructor Destructor Destructor 

即使发生副本删除,并且复制/移动构造函数未被调用,它也必须存在并且可访问(就好像根本没有发生任何优化),否则程序是不合格的。

您只能在不会影响软件可观察行为的地方使用这种复制方法。 复制elision是唯一允许有可观察到的副作用的优化forms。 例:

 #include <iostream> int n = 0; class ABC { public: ABC(int) {} ABC(const ABC& a) { ++n; } // the copy constructor has a visible side effect }; // it modifies an object with static storage duration int main() { ABC c1(21); // direct-initialization, calls C::C(42) ABC c2 = ABC(21); // copy-initialization, calls C::C( C(42) ) std::cout << n << std::endl; // prints 0 if the copy was elided, 1 otherwise return 0; } Output without -fno-elide-constructors root@ajay-PC:/home/ayadav# g++ -std=c++11 copy_elision.cpp root@ajay-PC:/home/ayadav# ./a.out 0 Output with -fno-elide-constructors root@ajay-PC:/home/ayadav# g++ -std=c++11 copy_elision.cpp -fno-elide-constructors root@ajay-PC:/home/ayadav# ./a.out 1 

GCC提供了-fno-elide-constructors elide -fno-elide-constructors选项来禁用复制elision。 如果你想避免可能的复制,可以使用-fno-elide-constructors elide -fno-elide-constructors

现在几乎所有编译器在启用优化时都提供复制精简(如果没有其他选项设置为禁用)。

结论

通过每个副本省略,复制的一个构造和一个匹配的销毁被省略,从而节省CPU时间,并且不创build一个对象,从而节省堆栈帧的空间。