459

什么是复制省略?什么是(命名的)返回值优化?它们意味着什么?

它们会在什么情况下发生?什么是限制?

4

5 回答 5

310

介绍

对于技术概述 -跳到这个答案

对于发生复制省略的常见情况,请跳至此答案

复制省略是大多数编译器实现的一种优化,用于在某些情况下防止额外的(可能是昂贵的)复制。它使按值返回或按值传递在实践中可行(有限制)。

这是省略(哈哈!)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();
}

根据编译器和设置,以下输出都是有效的:

你好世界!
制作了一份副本。
制作了一份副本。


你好世界!
制作了一份副本。


你好世界!

这也意味着可以创建更少的对象,因此您也不能依赖于调用特定数量的析构函数。您不应该在复制/移动构造函数或析构函数中包含关键逻辑,因为您不能依赖它们被调用。

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

C++17:从 C++17 开始,直接返回对象时保证复制省略:

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
}
于 2012-10-18T11:03:03.340 回答
112

Standard reference

For a less technical view & introduction - skip to this answer.

For common cases where copy elision occurs - skip to this answer.

Copy elision is defined in the standard in:

12.8 Copying and moving class objects [class.copy]

as

31) When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the copy/move constructor and/or destructor for the object have side effects. In such cases, the implementation treats the source and target of the omitted copy/move operation as simply two different ways of referring to the same object, and the destruction of that object occurs at the later of the times when the two objects would have been destroyed without the optimization.123 This elision of copy/move operations, called copy elision, is permitted in the following circumstances (which may be combined to eliminate multiple copies):

— in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) with the same cvunqualified type as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function’s return value

— in a throw-expression, when the operand is the name of a non-volatile automatic object (other than a function or catch-clause parameter) whose scope does not extend beyond the end of the innermost enclosing try-block (if there is one), the copy/move operation from the operand to the exception object (15.1) can be omitted by constructing the automatic object directly into the exception object

— when a temporary class object that has not been bound to a reference (12.2) would be copied/moved to a class object with the same cv-unqualified type, the copy/move operation can be omitted by constructing the temporary object directly into the target of the omitted copy/move

— when the exception-declaration of an exception handler (Clause 15) declares an object of the same type (except for cv-qualification) as the exception object (15.1), the copy/move operation 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.

123) Because only one object is destroyed instead of two, and one copy/move constructor is not executed, there is still one object destroyed for each one constructed.

The example given is:

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

and explained:

Here the criteria for elision can be combined to eliminate two calls to the copy constructor of class Thing: the copying of the local automatic object t into the temporary object for the return value of function f() and the copying of that temporary object into object t2. Effectively, the construction of the local object t can be viewed as directly initializing the global object t2, and that object’s destruction will occur at program exit. Adding a move constructor to Thing has the same effect, but it is the move construction from the temporary object to t2 that is elided.

于 2012-10-18T11:03:51.627 回答
109

Common forms of copy elision

For a technical overview - skip to this answer.

For a less technical view & introduction - skip to this answer.

(Named) Return value optimization is a common form of copy elision. It refers to the situation where an object returned by value from a method has its copy elided. The example set forth in the standard illustrates named return value optimization, since the object is named.

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

Regular return value optimization occurs when a temporary is returned:

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

Other common places where copy elision takes place is when a temporary is passed by value:

class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
void foo(Thing t);

foo(Thing());

or when an exception is thrown and caught by value:

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

Common limitations of copy elision are:

  • multiple return points
  • conditional initialization

Most commercial-grade compilers support copy elision & (N)RVO (depending on optimization settings).

于 2012-10-18T11:04:05.947 回答
69

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

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

  1. NRVO(命名返回值优化):如果函数按值返回类类型,并且返回语句的表达式是具有自动存储持续时间的非易失性对象的名称(这不是函数参数),则复制/移动可以省略将由非优化编译器执行的。如果是这样,则返回值直接在存储中构造,否则函数的返回值将被移动或复制到该存储中。
  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());    //RVO, not 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  

即使发生复制省略并且未调用复制/移动构造函数,它也必须存在且可访问(就好像根本没有发生优化一样),否则程序格式错误。

您应该只在不会影响软件可观察行为的地方允许此类复制省略。复制省略是唯一允许具有(即省略)可观察到的副作用的优化形式。例子:

#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禁用复制省略的选项。如果您想避免可能的复制省略,请使用-fno-elide-constructors.

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

结论

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

于 2015-01-13T07:26:05.853 回答
-2

在这里,我给出了我今天显然遇到的另一个复制省略示例。

# include <iostream>


class Obj {
public:
  int var1;
  Obj(){
    std::cout<<"In   Obj()"<<"\n";
    var1 =2;
  };
  Obj(const Obj & org){
    std::cout<<"In   Obj(const Obj & org)"<<"\n";
    var1=org.var1+1;
  };
};

int  main(){

  {
    /*const*/ Obj Obj_instance1;  //const doesn't change anything
    Obj Obj_instance2;
    std::cout<<"assignment:"<<"\n";
    Obj_instance2=Obj(Obj(Obj(Obj(Obj_instance1))))   ;
    // in fact expected: 6, but got 3, because of 'copy elision'
    std::cout<<"Obj_instance2.var1:"<<Obj_instance2.var1<<"\n";
  }

}

结果:

In   Obj()
In   Obj()
assignment:
In   Obj(const Obj & org)
Obj_instance2.var1:3
于 2020-10-15T14:27:18.550 回答