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

527
什么是复制省略?什么是(命名的)返回值优化?它们意味着什么?
在什么情况下可能会发生?有哪些限制?
  • 如果您被引用到这个问题,可能正在寻找 简介
  • 有关技术概述,请参见标准参考
  • 这里查看常见情况。

2
复制省略是一种看待它的方式;对象省略或对象融合(或混淆)是另一种观点。 - curiousguy
1
我发现这个链接很有帮助。 - subtleseeker
5个回答

366

介绍

要了解技术概述,请跳转到此答案

对于常见的省略复制情况,请跳转到此答案

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

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

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

你好世界!
已复制。
已复制。


你好世界!
已经复制了一份。
你好,世界!
这也意味着创建的对象数量较少,因此你不能依赖于特定数量的析构函数被调用。在复制/移动构造函数或析构函数中不应该有关键逻辑,因为你不能依赖于它们被调用。
如果对复制或移动构造函数的调用被省略了,那么该构造函数仍然必须存在并且可访问。这确保了复制省略不会允许复制通常不可复制的对象,例如因为它们具有私有或已删除的复制/移动构造函数。
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
3
编译器何时以及如何决定进行优化的方式。 - Luchian Grigore
21
@zhangxaochen,第一个输出:copy 1 来自于将返回结果复制到临时变量中,copy 2 来自于将临时变量复制到 obj 中;第二个则是当上述任意一个被优化后,可能会省略返回复制;第三个则是两者都被省略了。 - victor
4
在我看来,这一定是我们可以依赖的一个特性。因为如果我们不能依赖它,那么它将严重影响我们在现代C++(RVO vs std::move)中实现函数的方式。在观看CppCon 2014视频时,我真的有印象所有现代编译器都始终执行RVO。此外,我在某个地方读到即使没有进行任何优化,编译器也会应用RVO。但是,当然,我不确定这一点。这就是为什么我在问。 - j00hi
11
不要在返回语句中写入move操作 - 如果没有使用返回值优化,返回值默认会被移动出去。 - MikeMB
显示剩余11条评论

131

常见的拷贝省略形式

如需了解技术概述,请跳转至此答案

如需简单介绍,请跳转至此答案

(命名的)返回值优化是一种常见的拷贝省略形式。它指的是从方法中按值返回的对象被省略其拷贝的情况。标准中提出的示例说明了命名返回值优化,因为该对象已经命名。

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
@phonetagger,我已经链接到了MSDN文章,希望这能澄清一些问题。 - Luchian Grigore

123

标准参考

如果想了解更为通俗易懂的内容和导言 - 请跳转至此答案

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

复制省略 在标准中被定义在:

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

中,即

当满足特定条件时,实现可以省略类对象的复制/移动构造,即使该对象的复制/移动构造函数和/或析构函数具有副作用。在这种情况下,实现将省略的复制/移动操作的源和目标视为仅是引用同一对象的两种不同方式,并且该对象的销毁发生在没有优化的情况下两个对象将被销毁的时间中较晚的时间点。这种复制/移动操作的省略称为复制省略,可以在以下情况下使用(可以组合以消除多个副本):
- 在具有类返回类型的函数中的返回语句中,当表达式是非易失性自动对象(函数或catch子句参数之外)的名称,并且其cv-unqualified类型与函数返回类型相同时,可以通过直接将自动对象构造到函数的返回值中来省略复制/移动操作。 - 在throw表达式中,当操作数是非易失性自动对象(函数或catch子句参数之外)的名称,其作用域不延伸到最内层封闭try块的末尾时,可以通过直接将自动对象构造到异常对象中来省略从操作数到异常对象(15.1)的复制/移动操作。 - 当未绑定到引用(12.2)的临时类对象将被复制/移动到具有相同cv-unqualified类型的类对象时,可以通过直接将临时对象构造到省略的复制/移动操作的目标中来省略复制/移动操作。 - 当异常处理程序(第15条)的异常声明声明与异常对象(15.1)具有相同类型(除了cv限定符)的对象时,如果程序的含义除了由异常声明声明的对象的构造函数和析构函数的执行外不会改变,则可以将复制/移动操作省略为将异常声明视为异常对象的别名。
因为只销毁一个对象而不是两个对象,并且不执行一个复制/移动构造函数,所以仍然销毁每个已构造的对象的一个对象。

给出的示例是:

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

并解释:

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


3
这是来自C++17标准还是早期版本? - Nils
1
为什么如果函数参数和函数返回值类型相同,就无法进行返回值优化? - Sahil Singh
1
这篇文章试图回答以下问题 - https://dev59.com/3Gox5IYBdhLWcg3wFAfW - Sahil Singh
3
原始类型是否有任何复制省略?如果我有一个传播返回值(可能是错误代码)的函数,是否会有类似于对象的优化? - WARhead
将移动构造函数添加到Thing中具有相同的效果,但省略的是从临时对象到t2的移动构造。应该添加",并省略了从t到临时对象的移动构造"吗?还是我漏掉了什么? - Jake1234
@Jake1234 我认为你是对的。在没有复制省略的情况下,这两个构造都将使用移动构造函数(假设Thing有一个移动构造函数)。 - huangjl

77

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

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

  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  

即使发生拷贝省略并且未调用拷贝/移动构造函数,该函数必须存在并且可访问(好像没有进行任何优化一样),否则程序就是不合法的。

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

#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
语句ABC obj2(xyz123());是NRVO还是RVO?这个语句不会获得和ABC xyz = "Stack Overflow"; // RVO一样的临时变量/对象吗? - Asif Mushtaq
4
为了更加具体地说明RVO,您可以参考编译器生成的汇编代码(将编译器标志-fno-elide-constructors更改为查看差异)。https://godbolt.org/g/Y2KcdH - Gab是好人
3
ABC xyz = "Stack Overflow"隐式调用的是ABC::ABC(const char *ptr),而不是返回值优化(RVO)。 - user1079475
对于ABC xyz = "Stack Overflow",它调用了显式定义的拷贝构造函数,所以我不确定这是否可以是RVO(返回值优化),而且R-这个词表明函数有一个返回值,然而构造函数并没有返回值。 - Nusrat Nuriyev
更有趣的是,ABC obj1(fun123())这个可以省略,然而在C++17和C++20中将其作为可选项保留,所以如果-fno-elide-constructors打开,则会调用def + move构造函数。 - Nusrat Nuriyev

-1

这里我提供另一个复制省略的例子,显然今天我遇到了。

# 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
这已经包含在Luchian的答案中了(通过值传递的临时对象)。 - Toby Speight
这个答案可以作为其他答案的附注。它展示了一个有趣的扩展案例。在C++14中,打开-fno-elide-constructors选项会得到6,而不使用该选项则得到3。在C++17及以上版本中,始终得到3。 - Dhwani Katagade

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接