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

Luc*_*ore 350 c++ optimization c++-faq return-value-optimization copy-elision

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

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

Luc*_*ore 223

介绍

有关技术概述 - 请跳至此答案.

对于发生复制省略的常见情况 - 跳到此答案.

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

这是唯一的优化形式,即使复制/移动对象具有副作用,也可以应用(ha!)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();
}
Run Code Online (Sandbox Code Playgroud)

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

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


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


你好,世界!

这也意味着可以创建更少的对象,因此您也不能依赖于被调用的特定数量的析构函数.你不应该在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
}
Run Code Online (Sandbox Code Playgroud)

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

Luc*_*ore 91

标准参考

对于技术较少的观点和介绍 - 跳过这个答案.

对于发生复制省略的常见情况 - 跳到此答案.

复制省略在标准中定义:

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

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

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

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

- 当一个未绑定到引用(12.2)的临时类对象被复制/移动到具有相同cv-nonqualified类型的类对象时,可以通过将临时对象直接构造到该对象中来省略复制/移动操作.省略的复制/移动的目标

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

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

给出的例子是:

class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
Thing f() {
  Thing t;
  return t;
}
Thing t2 = f();
Run Code Online (Sandbox Code Playgroud)

并解释说:

这里可以组合elision的标准来消除对类的复制构造函数的两次调用Thing:将本地自动对象复制t到临时对象中以获取函数的返回值f() 以及将该临时对象复制到对象中t2.实际上,t 可以将本地对象的构造视为直接初始化全局对象t2,并且该对象的销毁将在程序退出时发生.向Thing添加移动构造函数具有相同的效果,但它是从临时对象移动构造到t2省略的.

  • 这是来自 C++17 标准还是来自早期版本? (3认同)
  • 原始类型是否有任何类型的复制省略?如果我有一个传播返回值(可能是错误代码)的函数,是否会有类似于对象的优化? (3认同)

Luc*_*ore 84

复制省略的常见形式

有关技术概述 - 请跳至此答案.

对于技术较少的观点和介绍 - 跳过这个答案.

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

class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
Thing f() {
  Thing t;
  return t;
}
Thing t2 = f();
Run Code Online (Sandbox Code Playgroud)

返回临时时会发生常规返回值优化:

class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
Thing f() {
  return Thing();
}
Thing t2 = f();
Run Code Online (Sandbox Code Playgroud)

复制省略发生的其他常见地方是临时值按值传递:

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

foo(Thing());
Run Code Online (Sandbox Code Playgroud)

或者抛出异常并按值捕获时:

struct Thing{
  Thing();
  Thing(const Thing&);
};

void foo() {
  Thing c;
  throw c;
}

int main() {
  try {
    foo();
  }
  catch(Thing c) {  
  }             
}
Run Code Online (Sandbox Code Playgroud)

复制省略的常见限制是:

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

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

  • 我有兴趣看到"常见限制"要点解释了一下......是什么造成这些限制因素? (3认同)

Aja*_*dav 48

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

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

  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());//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  
Run Code Online (Sandbox Code Playgroud)

即使发生了复制省略并且未调用复制/移动构造函数,它也必须存在并且可访问(就好像根本没有进行优化),否则程序就会形成错误.

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

#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
Run Code Online (Sandbox Code Playgroud)

GCC提供-fno-elide-constructors禁用复制省略的选项.如果您想避免可能的复制省略,请使用-fno-elide-constructors.

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

结论

对于每个复制省略,省略了一个构造和一个匹配的复制销毁,从而节省了CPU时间,并且没有创建一个对象,因此节省了堆栈帧上的空间.

  • 声明`ABC obj2(xyz123());`它是NRVO还是RVO?是不是得到临时变量/对象与`ABC xyz ="Stack Overflow"; // RVO`相同 (5认同)
  • ABC xyz = “Stack Overflow”不是吗?只是隐式调用 ABC::ABC(const char *ptr) 而不是 RVO? (3认同)
  • 要更具体地说明RVO,可以参考编译器生成的程序集(更改编译器标志-fno-elide-constructors以查看diff).https://godbolt.org/g/Y2KcdH (2认同)