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

时间:2012-10-18 11:03:03

标签: c++ optimization c++-faq return-value-optimization copy-elision

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

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

5 个答案:

答案 0 :(得分:209)

简介

技术概述 - skip to this answer

对于发生复制省略的常见情况 - skip to this answer

复制省略是大多数编译器实施的优化,以防止在某些情况下额外(可能很昂贵)的副本。它使得按值或按值传递在实践中可行(限制适用)。

这是唯一的优化形式,即使复制/移动对象具有副作用,也可以应用(ha!)as-if规则 - 复制省略。

以下示例取自Wikipedia

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

取决于编译器和&amp;设置,以下输出均有效

  

Hello World!
  制作了一份副本。
  制作了一份副本。


  

Hello World!
  制作了副本。


  

Hello World!

这也意味着可以创建更少的对象,因此您也不能依赖于被调用的特定数量的析构函数。你不应该在copy / move-constructors或析构函数中有关键逻辑,因为你不能依赖它们被调用。

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

C ++ 17 :从C ++ 17开始,当直接返回对象时,可以保证Copy Elision:

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
}

答案 1 :(得分:85)

标准参考

对于技术性较差的观点&amp;介绍 - skip to this answer

对于发生复制省略的常见情况 - skip to this answer

复制省略在标准中定义:

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

作为

  

31)当满足某些条件时,允许实现省略类的复制/移动构造   对象,即使对象的复制/移动构造函数和/或析构函数具有副作用。在这种情况下,   该实现将省略的复制/移动操作的源和目标视为两个不同的   引用相同对象的方式,以及对象的破坏发生在时间的晚期   当两个对象在没有优化的情况下被破坏时。 123 这个复制/移动的省略   在以下情况下允许称为 copy elision 的操作(可以合并到   消除多份副本):

     

- 在具有类返回类型的函数的return语句中,当表达式是a的名称时   具有相同cvunqualified的非易失性自动对象(函数或catch子句参数除外)   键入函数返回类型,可以通过构造省略复制/移动操作   自动对象直接进入函数的返回值

     

- 在throw-expression中,当操作数是非易失性自动对象的名称时(除了   function或catch-clause参数),其范围不超出最内层的末尾   封闭try-block(如果有的话),从操作数到异常的复制/移动操作   通过将自动对象直接构造到异常对象

中,可以省略object(15.1)      

- 当复制/移动尚未绑定到引用(12.2)的临时类对象时   对于具有相同cv-unqualified类型的类对象,可以省略复制/移动操作   将临时对象直接构造到省略的复制/移动

的目标中      

- 当异常处理程序的异常声明(第15节)声明一个相同类型的对象时   (除了cv-qualification)作为异常对象(15.1),可以省略复制/移动操作   通过将异常声明视为异常对象的别名(如果程序的含义)   除了为声明的对象执行构造函数和析构函数之外,它将保持不变   异常声明。

     

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添加移动构造函数具有相同的效果,但它是移动构造   <{1}}的临时对象,已被删除。

答案 2 :(得分:78)

复制省略的常见形式

技术概述 - skip to this answer

对于技术性较差的观点&amp;介绍 - skip to this answer

(命名)返回值优化是复制省略的常见形式。它指的是通过方法的值返回的对象的副本被省略的情况。标准中提出的示例说明了命名的返回值优化,因为该对象已命名。

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

或者抛出异常并按值

抓住

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:

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

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

答案 3 :(得分:43)

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

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

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

    即使发生了复制省略并且没有调用copy- / move-构造函数,它也必须存在且可访问(好像根本没有进行优化),否则程序就会格式不正确。

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

    #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时间,并且没有创建一个对象,从而节省了堆栈框架上的空间。

答案 4 :(得分:0)

在这里,我再举一个今天显然遇到的复制删除示例。

# 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