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


377

什么是复制省略?什么是(命名)返回值优化?他们暗示什么?

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


1
复制省略是一种查看方式。对象省略或对象融合(或混乱)是另一种观点。
curiousguy

我发现此链接很有帮助。
细微搜索

Answers:


246

介绍

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

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

复制省略是大多数编译器实现的一种优化,用于在某些情况下防止多余的副本(可能代价很高)。在实践中,这使按价值回报或按价值传递成为可行(有限制)。

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

以下示例摘自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
}

2
您能否解释一下何时发生第二个输出以及何时发生第三个输出?
zhangxaochen

3
@zhangxaochen编译器何时以及如何决定以这种方式进行优化。
Luchian Grigore 2014年

10
@zhangxaochen,第一个输出:复制1是从返回到temp,复制2是从temp到obj;第二是当上述之一被拒绝时,可能删除reutnr副本;三者都被淘汰
胜利者

2
嗯,但是我认为,这必须是我们可以依靠的功能。因为如果不能这样做,将会严重影响我们在现代C ++中实现函数的方式(RVO与std :: move)。在观看一些CppCon 2014视频时,我的确给人一种印象,即所有现代编译器都始终执行RVO。此外,我读过某处在没有任何优化的情况下,编译器也会应用它。但是,当然,我不确定。这就是为什么我问。
j00hi 2015年

8
@ j00hi:切勿在return语句中写入move-如果未应用rvo,则无论如何默认情况下都将返回值移出。
MikeMB

96

标准参考

对于较不技术的观点和介绍,请跳至此答案

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

标准中定义了复制省略

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

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

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

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

—当尚未绑定到引用(12.2)的临时类对象将被复制/移动到具有相同cv-unqualtype类型的类对象时,可以通过将临时对象直接构造为以下形式来省略复制/移动操作:省略复制/移动的目标

—当异常处理程序的异常声明(第15条)声明与异常对象(15.1)相同类型的对象(cv限定除外)时,可以通过处理异常声明来省略复制/移动操作如果程序的含义将保持不变,则将其作为异常对象的别名,除了针对异常声明所声明的对象的构造函数和析构函数的执行以外。

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

给出的示例是:

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

并解释:

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


1
是来自C ++ 17标准还是来自早期版本?
尼尔斯

90

复制省略的常见形式

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

对于较不技术的观点和介绍,请跳至此答案

(命名)返回值优化是复制省略的一种常见形式。它是指从方法返回的值删除了对象的情况。标准中阐述的示例说明了命名返回值优化,因为对象是命名的。

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) {  
  }             
}

复制省略的常见限制是:

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

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


4
我对看到“常见限制”的要点仅是一点点解释感兴趣...是什么导致了这些限制因素?
phonetagger

我针对msdn文章链接了@phonetagger,希望能清除一些内容。
Luchian Grigore

54

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

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

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


6
声明 ABC obj2(xyz123());是NRVO还是RVO?是否没有获得与ABC xyz = "Stack Overflow";//RVO
Asif Mushtaq

3
要更详细地说明RVO,可以引用编译器生成的程序集(更改编译器标志-fno-elide-constructors以查看diff)。godbolt.org/g/Y2KcdH
Gab是好人
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.