【问题标题】:What are copy elision and return value optimization?什么是复制省略和返回值优化?
【发布时间】:2021-10-29 22:57:16
【问题描述】:

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

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

【问题讨论】:

  • 复制省略是查看它的一种方式;对象省略或对象融合(或混淆)是另一种观点。
  • 我发现这个link 很有帮助。

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


【解决方案1】:

简介

技术概览 - skip to this answer

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

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

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

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

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


世界你好!
已制作副本。


世界你好!

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

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

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
}

【讨论】:

  • 能否请您解释一下第二次输出何时发生以及第三次输出何时发生?
  • @zhangxaochen 编译器何时以及如何决定以这种方式进行优化。
  • @zhangxaochen,第一个输出:copy 1是从return到一个temp,copy 2是从temp到obj;第二是优化上述之一时,可能会省略reutnr副本; thris 都被省略了
  • 嗯,但在我看来,这必须是我们可以依赖的功能。因为如果我们不能,它将严重影响我们在现代 C++ 中实现函数的方式(RVO 与 std::move)。在观看 CppCon 2014 的一些视频时,我真的得到了所有现代编译器总是做 RVO 的印象。此外,我在某处也读过,编译器应用它也没有任何优化。但是,当然,我不确定。这就是我问的原因。
  • @j00hi:永远不要在 return 语句中写 move - 如果没有应用 rvo,默认情况下返回值会被移出。
【解决方案2】:

标准参考

技术含量较低的观点和介绍 - skip to this answer

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

复制省略在标准中定义:

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

作为

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

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

— 在 throw 表达式中,当操作数是非易失性自动对象的名称时(除了 函数或 catch 子句参数),其范围不超出最内层的末尾 封闭的try-block(如果有的话),从操作数到异常的复制/移动操作 对象(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();

并解释:

这里可以结合省略的条件来消除对类Thing的复制构造函数的两次调用: 将本地自动对象t复制到临时对象中,用于函数f()的返回值 并将该临时对象复制到对象t2 中。有效地构造了本地对象t 可以看成是直接初始化全局对象t2,这个对象的销毁会在程序中发生 出口。给 Thing 添加一个 move 构造函数也有同样的效果,但它是从 t2 的临时对象被省略。

【讨论】:

  • 是来自 C++17 标准还是来自早期版本?
  • 为什么函数参数与函数返回类型相同,不能进行返回值优化?
  • 原始类型是否有任何类型的复制省略?如果我有一个传播返回值的函数(可能是错误码),会不会有类似对象的优化?
【解决方案3】:

复制省略的常见形式

技术概览 - skip to this answer

技术含量较低的观点和介绍 - 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:

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

大多数商业级编译器支持复制省略和 (N)RVO(取决于优化设置)。

【讨论】:

  • 我很想看一下“常见限制”要点的解释......是什么造成了这些限制因素?
  • @phonetagger 我链接到了 msdn 文章,希望能清除一些东西。
【解决方案4】:

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

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

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

【讨论】:

  • 声明 ABC obj2(xyz123()); 是 NRVO 还是 RVO?是否没有得到与ABC xyz = "Stack Overflow";//RVO 相同的临时变量/对象
  • 要更具体地说明 RVO,您可以参考编译器生成的程序集(更改编译器标志 -fno-elide-constructors 以查看差异)。 godbolt.org/g/Y2KcdH
  • 不是 ABC xyz = "堆栈溢出";只是对 ABC::ABC(const char *ptr) 而不是 RVO 的隐式调用?
【解决方案5】:

这里我再举一个我今天显然遇到的复制省略的例子。

# 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

【讨论】:

  • 这已经包含在 Luchian 的答案中(按值传递的临时对象)。
猜你喜欢
相关资源
最近更新 更多