C++ 什么是复制省略和返回值优化?
什么是复制省略?什么叫回报值优化?它们意味着什么 在什么情况下会发生这种情况?限制是什么 如果你提到这个问题,你可能在寻找。 有关技术概述,请参阅。 看见 介绍 有关技术概述- 对于发生复制省略的常见情况- 拷贝省略是大多数编译器实现的一种优化,用于在某些情况下防止额外的可能昂贵的拷贝。它使得按值返回或按值传递在实际应用中可行 这是elides ha的唯一优化形式!即使复制/移动对象有副作用,也可以应用“仿佛规则-复制省略” 以下示例取自: 根据编译器和设置,以下输出均有效: 你好,世界! 复制了一份。 复制了一份 你好,世界! 复制了一份 你好,世界 这也意味着可以创建更少的对象,因此您也不能依赖于调用特定数量的析构函数。复制/移动构造函数或析构函数中不应该有关键逻辑,因为不能依赖于调用它们 如果省略了对复制或移动构造函数的调用,则该构造函数必须仍然存在并且可以访问。这确保复制省略不允许复制通常不可复制的对象,例如,因为它们具有私有或已删除的复制/移动构造函数 C++17:从C++17开始,当直接返回对象时,可以保证省略复制:C++ 什么是复制省略和返回值优化?,c++,optimization,c++-faq,return-value-optimization,copy-elision,C++,Optimization,C++ Faq,Return Value Optimization,Copy Elision,什么是复制省略?什么叫回报值优化?它们意味着什么 在什么情况下会发生这种情况?限制是什么 如果你提到这个问题,你可能在寻找。 有关技术概述,请参阅。 看见 介绍 有关技术概述- 对于发生复制省略的常见情况- 拷贝省略是大多数编译器实现的一种优化,用于在某些情况下防止额外的可能昂贵的拷贝。它使得按值返回或按值传递在实际应用中可行 这是elides ha的唯一优化形式!即使复制/移动对象有副作用,也可以应用“仿佛规则-复制省略” 以下示例取自: 根据编译器和设置,以下输出均有效: 你好,世界! 复制
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]
作为
31当满足某些条件时,允许实现省略类的复制/移动构造
对象,即使该对象的复制/移动构造函数和/或析构函数有副作用。在这种情况下,
该实现将省略的复制/移动操作的源和目标视为两个不同的操作
指代同一对象的方式,并且该对象的销毁发生在时间较晚的时候
如果没有优化,这两个对象将被销毁。123这省略了复制/移动
在下列情况下,允许进行称为复制省略的操作,这些情况可能会合并到
消除多个副本:
-在具有类返回类型的函数中的return语句中,当表达式是
非易失性自动对象,而不是具有相同参数的函数或catch子句参数
类型作为函数返回类型,可以通过构造
自动对象直接输入函数的返回值
-在抛出表达式中,当操作数是非易失性自动对象的名称而不是
函数或catch子句参数,其范围不超出最内层
封闭try块(如果有)从操作数到异常的复制/移动操作
通过将自动对象直接构造到异常对象中,可以省略对象15.1
-复制/移动未绑定到参考12.2的临时类对象时
对于具有相同cv类型的类对象,可以通过
将临时对象直接构造到省略的复制/移动的目标中
-当异常处理程序的异常声明第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返回值的临时对象中
以及将临时对象复制到对象t2中。有效地,局部对象t的构造
可以视为直接初始化全局对象t2,并且该对象的销毁将在程序中发生
出口向对象添加移动构造函数具有相同的效果,但它是从
删除t2的临时对象
抄袭省略的常见形式
技术概述
ew-
对于技术性较低的视图和介绍-
命名返回值优化是一种常见的拷贝省略形式。它指的是从方法返回值的对象的副本被省略的情况。标准中给出的示例演示了命名返回值优化,因为对象是命名的
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();
发生复制省略的其他常见位置是通过值传递临时值时:
或者当值引发并捕获异常时:
多个返回点
条件初始化
大多数商用级编译器支持复制省略&NRVO,具体取决于优化设置。复制省略是一种编译器优化技术,可消除不必要的对象复制/移动 在以下情况下,允许编译器忽略复制/移动操作,因此不调用关联的构造函数: NRVO命名返回值优化:如果函数按值返回类类型,并且返回语句的表达式是具有自动存储持续时间的非易失性对象的名称,而该对象不是函数参数,则可以省略将由非优化编译器执行的复制/移动。如果是这样,返回值将直接在存储中构造,否则函数的返回值将被移动或复制到存储中。 RVO返回值优化:如果函数返回一个无名的临时对象,该对象将由一个朴素的编译器移动或复制到目标中,则可以根据1省略复制或移动。 即使在发生复制省略且未调用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构造函数选项来禁用复制省略。
如果要避免可能的复制省略,请使用-fno-elide构造函数
现在,几乎所有的编译器都在启用优化时提供拷贝省略,如果没有其他选项设置为禁用它
结论
对于每个副本省略,省略了副本的一个构造和一个匹配的销毁,从而节省了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";
}
}
我很想看看常见的局限性要点解释一下。。。是什么造成了这些限制因素?@phonetagger我与msdn文章链接,希望能澄清一些问题。你能解释一下第二次输出是什么时候发生的,第三次输出是什么时候发生的吗?@zhangxaochen编译器决定以这种方式进行优化的时间和方式。@zhangxaochen,第一次输出:复制1是从返回到临时,复制2是从临时到obj;第二种情况是,当上述其中一种被优化时,可能会忽略reutnr副本;第三个都是省略的嗯,但在我看来,这一定是我们可以依赖的功能。因为如果我们不能,它将严重影响我们在现代C++ RVO与STD::移动中实现我们的功能的方式。在观看CppCon 2014的一些视频时,我真的得到了这样的印象:所有现代编译器都会执行RVO。此外,我在某个地方读到,在没有任何优化的情况下,编译器也会应用它。但是,当然,我不确定。这就是我为什么要问的原因。@j00hi:永远不要在返回语句中写入move-如果未应用rvo,返回值在默认情况下也会移出。复制省略是查看它的一种方法;对象省略或对象融合或混淆是另一种观点。我发现这很有帮助;是NRVO还是RVO?它得到的临时变量/对象是否与ABC xyz=堆栈溢出相同//RVO要对RVO进行更具体的说明,您可以参考编译器生成的程序集,更改编译器标志-fno elide构造函数以查看差异。这不是ABC xyz=堆栈溢出;只是对ABC::ABCconst char*ptr的隐式调用,而不是RVO?这是来自C++17标准还是早期版本?如果函数参数的返回类型与函数的返回类型相同,为什么不能对其返回值进行优化?这试图回答-基元类型是否存在任何类型的复制省略?如果我有一个传播返回值的函数,可能是一个错误代码,那么会有类似于对象的优化吗?这已经包含在Luchian的答案“通过值传递的临时对象”中。
struct Thing{
Thing();
Thing(const Thing&);
};
void foo() {
Thing c;
throw c;
}
int main() {
try {
foo();
}
catch(Thing c) {
}
}
#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
# 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