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

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

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

130043 次浏览

简介

对于一个技术概述- 直接跳到这个答案

对于发生复制省略的常见情况- 直接跳到这个答案

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

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

取决于编译器&设置后,输出都是有效的:

< p > Hello World !
复印了一份。
复印了一份。
< / p >

< p > Hello World !

.日志含义

你好世界!

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

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

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
}

标准参考

对于一个不那么技术性的观点&引入- 直接跳到这个答案

对于发生复制省略的常见情况- 直接跳到这个答案

复制省略在标准中定义:

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

作为

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

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

-在抛出表达式中,当操作数是一个非易失性自动对象的名称时(a 函数或catch-子句参数),其作用域不超出最内层的末端 外围try块(如果有的话),从操作数到异常的复制/移动操作 Object(15.1)可以通过将自动对象直接构造到异常对象

来省略

-没有绑定到引用(12.2)的临时类对象将被复制/移动 对于具有相同cv- unrestricted类型的类对象,复制/移动操作可以被省略 将临时对象直接构造到省略的copy/move

的目标中

-当异常处理程序的异常声明(第15条)声明了相同类型的对象时 (除了cv-qualification)作为异常对象(15.1),复制/移动操作可以省略 通过将异常声明作为异常对象的别名处理,如果程序的意义 所声明的对象的构造函数和析构函数的执行将是不变的 exception-declaration。< / p >

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

给出的例子是:

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

和解释:

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

常见的复制省略形式

对于一个技术概述- 直接跳到这个答案

对于一个不那么技术性的观点&引入- 直接跳到这个答案

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

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强制执行了上述许多复制省略类。

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

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

  1. 命名返回值优化:如果函数按值返回类类型,并且return语句的表达式是具有自动存储持续时间的非易变对象的名称(不是函数形参),则可以省略由非优化编译器执行的复制/移动。如果是,则直接在函数的返回值将被移动或复制到的存储中构造返回值。
  2. RVO(返回值优化):如果函数返回一个无名的临时对象,该对象将由naive编译器移动或复制到目标,则复制或移动可以按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-构造函数,它也必须存在并可访问(就好像根本没有发生优化一样),否则程序就是病态的。

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

#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.

现在几乎所有的编译器都在启用优化时提供拷贝省略(如果没有其他选项设置为禁用它)。

结论< h1 id = "结论" > < / h1 >

对于每个副本省略,省略了副本的一次构造和一次匹配的销毁,从而节省了CPU时间,并且不创建一个对象,从而节省了堆栈帧上的空间。

在这里,我给出我今天显然遇到的另一个复制省略的例子。

# 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