提问人:Luchian Grigore 提问时间:10/18/2012 最后编辑:CommunityLuchian Grigore 更新时间:6/8/2023 访问量:152818
什么是复制省略和返回值优化?
What are copy elision and return value optimization?
问:
什么是复制省略?什么是(命名的)返回值优化?它们意味着什么?
它们会在什么情况下发生?什么是限制?
答:
介绍
有关技术概述 - 跳到此答案。
对于发生复制省略的常见情况 - 跳到此答案。
复制省略是大多数编译器实现的优化,用于防止在某些情况下出现额外(可能昂贵的)复制。它使按值返回或按值传递在实践中变得可行(有限制)。
这是唯一一种省略(哈!)假如规则的优化形式 - 即使复制/移动对象有副作用,也可以应用复制省略。
以下示例摘自维基百科:
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
}
评论
标准参考
对于不太技术性的观点和介绍 - 跳到这个答案。
对于发生复制省略的常见情况 - 跳到此答案。
复制省略在标准中定义:
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 构造函数具有相同的效果,但它是 临时对象被省略。
Thing
t
f()
t2
t
t2
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 使上述许多类的复制省略成为强制性的。
评论
复制省略是一种编译器优化技术,可消除不必要的对象复制/移动。
在以下情况下,允许编译器省略复制/移动操作,因此不调用关联的构造函数:
- NRVO(命名返回值优化):如果函数按值返回类类型,并且返回语句的表达式是具有自动存储持续时间的非易失性对象的名称(不是函数参数),则可以省略由非优化编译器执行的复制/移动。如果是这样,则返回值将直接在存储中构造,否则函数的返回值将被移动或复制到该存储中。
- 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 时间,并且不会创建一个对象,从而节省堆栈帧上的空间。
评论
ABC obj2(xyz123());
ABC xyz = "Stack Overflow";//RVO
在这里,我举了另一个我今天显然遇到的复制省略的例子。
# 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
评论