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

What are copy elision and return value optimization?

提问人:Luchian Grigore 提问时间:10/18/2012 最后编辑:CommunityLuchian Grigore 更新时间:6/8/2023 访问量:152818

问:

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

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

C 返回值优化 copy-elision

评论

2赞 curiousguy 8/26/2015
复制省略是查看它的一种方式;对象省略或对象融合(或混淆)是另一种视图。
1赞 subtleseeker 3/2/2020
我发现这个链接很有帮助。

答:

366赞 Luchian Grigore 10/18/2012 #1

介绍

有关技术概述 - 跳到此答案

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

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

这是唯一一种省略(哈!)假如规则的优化形式 - 即使复制/移动对象有副作用,也可以应用复制省略

以下示例摘自维基百科

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
}

评论

5赞 zhangxaochen 6/19/2014
您能否解释一下第二次输出何时发生,第三次输出何时发生?
3赞 Luchian Grigore 6/19/2014
@zhangxaochen编译器何时以及如何决定以这种方式进行优化。
21赞 victor 11/8/2014
@zhangxaochen,第 1 个输出:副本 1 是从 Resume 到 A Temp,Copy 2 从 Temp 到 obj;第二种情况是,当上述情况之一被省略时,可能省略了 reutnr 副本;Thris都被省略了
4赞 j00hi 2/5/2015
嗯,但在我看来,这一定是我们可以依赖的功能。因为如果我们做不到,它将严重影响我们在现代 C++ 中实现函数的方式(RVO 与 std::move)。在观看 CppCon 2014 的一些视频时,我真的觉得所有现代编译器总是做 RVO。此外,我在某处读到,编译器在没有任何优化的情况下应用了它。但是,当然,我不确定。这就是我问的原因。
11赞 MikeMB 3/10/2015
@j00hi:永远不要在 return 语句中写入 move - 如果未应用 rvo,则默认情况下无论如何都会移出返回值。
123赞 Luchian Grigore 10/18/2012 #2

标准参考

对于不太技术性的观点和介绍 - 跳到这个答案

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

复制省略在标准中定义:

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

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

— 在具有类返回类型的函数的 return 语句中,当表达式是 具有相同 cvunqualified 的非易失性自动对象(函数或 catch-clause 参数除外) type 作为函数返回类型,可以通过构造 自动对象直接进入函数的返回值

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

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

— 当异常处理程序的异常声明(第 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();

并解释说:

在这里,省略的条件可以组合起来,以消除对类的复制构造函数的两次调用: 将本地自动对象复制到临时对象中以返回函数的值,并将该临时对象复制到对象中。实际上,局部对象的构造可以看作是直接初始化全局对象,并且该对象的销毁将在程序中发生 退出。向 Thing 添加 move 构造函数具有相同的效果,但它是 临时对象被省略。Thingtf()t2tt2t2

评论

3赞 Nils 5/7/2019
这是来自 C++17 标准还是来自早期版本?
1赞 Sahil Singh 6/21/2020
如果函数参数与函数的返回类型相同,为什么不能优化返回值?
1赞 Sahil Singh 6/21/2020
这试图回答 - stackoverflow.com/questions/9444485/......
3赞 WARhead 8/3/2020
对于基元类型,是否有任何类型的复制省略?如果我有一个传播返回值(可能是错误代码)的函数,是否会有类似于对象的优化?
0赞 Jake1234 10/12/2022
“向 Thing 添加移动构造函数具有相同的效果,但省略的是从临时对象到 t2 的移动构造”,应添加“,以及省略的从 t 到临时的移动构造”。?还是我错过了什么?
131赞 Luchian Grigore 10/18/2012 #3

复制省略的常见形式

有关技术概述 - 跳到此答案

对于不太技术性的观点和介绍 - 跳到这个答案

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

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

Thing t2 = Thing();
Thing t3 = Thing(Thing()); // two rounds of elision
foo(Thing()); // parameter constructed from temporary

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

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

复制省略的常见限制包括:

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

大多数商业级编译器都支持复制省略和(N)RVO(取决于优化设置)。C++17 使上述许多类的复制省略成为强制性的。

评论

8赞 phonetagger 1/17/2013
我很想看到“常见限制”要点的解释......是什么造就了这些限制因素?
0赞 Luchian Grigore 1/17/2013
@phonetagger我链接到 msdn 文章,希望能清除一些东西。
77赞 Ajay yadav 1/13/2015 #4

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

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

  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  

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

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

#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 时间,并且不会创建一个对象,从而节省堆栈帧上的空间。

评论

9赞 Asif Mushtaq 8/28/2015
声明是 NRVO 还是 RVO?它是否没有得到与ABC obj2(xyz123());ABC xyz = "Stack Overflow";//RVO
4赞 Gab是好人 12/4/2016
若要更具体地说明 RVO,可以参考编译器生成的程序集(更改编译器标志 -fno-elide-constructors 以查看差异)。godbolt.org/g/Y2KcdH
3赞 user1079475 8/28/2020
不是 ABC xyz = “Stack Overflow”;只是对 ABC::ABC(const char *ptr) 而不是 RVO 的隐式调用?
0赞 Nusrat Nuriyev 7/18/2023
对于 ABC xyz = “Stack Overflow”;是调用显式定义的复制构造函数,所以我不确定这怎么可能是 RVO,并且单词 R- 表明函数有一个返回值,但是构造函数没有返回值。
0赞 Nusrat Nuriyev 7/18/2023
更有趣的是,ABC obj1( fun123()) 可以省略这一点,但是在 C++17 和 C++20 上将其保留为可选,因此如果 -fno-elide-constructors 为 ON,则将调用 def + move 构造函数。
-1赞 K.Karamazen 10/15/2020 #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

评论

2赞 Toby Speight 12/23/2020
这已经包含在 Luchian 的答案中(按值传递的临时对象)。
0赞 Dhwani Katagade 12/8/2022
这个答案可以作为其他答案的旁注。它显示了一个有趣的扩展案例。在 C++14 中,打开 -fno-elide-constructors 会得到 6,如果没有选项,我们会得到 3。在 C++17 及更高版本中,我们总是得到 3。