复制省略是什么?什么是(命名)返回值优化?他们意味着什么?
它们会在什么情况下发生?有什么限制?
答案 0 :(得分:209)
技术概述 - skip to this answer。
对于发生复制省略的常见情况 - skip to this answer。
复制省略是大多数编译器实施的优化,以防止在某些情况下额外(可能很昂贵)的副本。它使得按值或按值传递在实践中可行(限制适用)。
这是唯一的优化形式,即使复制/移动对象具有副作用,也可以应用(ha!)as-if规则 - 复制省略。
以下示例取自Wikipedia:
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();
}
取决于编译器和&amp;设置,以下输出均有效:
Hello World!
制作了一份副本。
制作了一份副本。
Hello World!
制作了副本。
Hello World!
这也意味着可以创建更少的对象,因此您也不能依赖于被调用的特定数量的析构函数。你不应该在copy / move-constructors或析构函数中有关键逻辑,因为你不能依赖它们被调用。
如果省略对副本或移动构造函数的调用,则该构造函数必须仍然存在且必须可访问。这确保了复制省略不允许复制通常不可复制的对象,例如,因为他们有私人或删除的复制/移动构造函数。
C ++ 17 :从C ++ 17开始,当直接返回对象时,可以保证Copy Elision:
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
}
答案 1 :(得分:85)
对于技术性较差的观点&amp;介绍 - skip to this answer。
对于发生复制省略的常见情况 - skip to this answer。
复制省略在标准中定义:
作为
31)当满足某些条件时,允许实现省略类的复制/移动构造 对象,即使对象的复制/移动构造函数和/或析构函数具有副作用。在这种情况下, 该实现将省略的复制/移动操作的源和目标视为两个不同的 引用相同对象的方式,以及对象的破坏发生在时间的晚期 当两个对象在没有优化的情况下被破坏时。 123 这个复制/移动的省略 在以下情况下允许称为 copy elision 的操作(可以合并到 消除多份副本):
- 在具有类返回类型的函数的return语句中,当表达式是a的名称时 具有相同cvunqualified的非易失性自动对象(函数或catch子句参数除外) 键入函数返回类型,可以通过构造省略复制/移动操作 自动对象直接进入函数的返回值
- 在throw-expression中,当操作数是非易失性自动对象的名称时(除了 function或catch-clause参数),其范围不超出最内层的末尾 封闭try-block(如果有的话),从操作数到异常的复制/移动操作 通过将自动对象直接构造到异常对象
中,可以省略object(15.1)- 当复制/移动尚未绑定到引用(12.2)的临时类对象时 对于具有相同cv-unqualified类型的类对象,可以省略复制/移动操作 将临时对象直接构造到省略的复制/移动
的目标中- 当异常处理程序的异常声明(第15节)声明一个相同类型的对象时 (除了cv-qualification)作为异常对象(15.1),可以省略复制/移动操作 通过将异常声明视为异常对象的别名(如果程序的含义) 除了为声明的对象执行构造函数和析构函数之外,它将保持不变 异常声明。
123)因为只有一个对象被破坏而不是两个,并且没有执行一个复制/移动构造函数,所以仍然有一个 为每一个构建的对象销毁。
给出的例子是:
class Thing {
public:
Thing();
~Thing();
Thing(const Thing&);
};
Thing f() {
Thing t;
return t;
}
Thing t2 = f();
并解释说:
这里可以合并elision的标准,以消除对类
Thing
的复制构造函数的两次调用: 将本地自动对象t
复制到临时对象中以获取函数f()
的返回值 并将该临时对象复制到对象t2
中。实际上,构造了本地对象t
可以看作是直接初始化全局对象t2
,并且该对象的破坏将在程序中发生 出口。向Thing添加移动构造函数具有相同的效果,但它是移动构造 <{1}}的临时对象,已被删除。
答案 2 :(得分:78)
技术概述 - skip to this answer。
对于技术性较差的观点&amp;介绍 - skip to this answer。
(命名)返回值优化是复制省略的常见形式。它指的是通过方法的值返回的对象的副本被省略的情况。标准中提出的示例说明了命名的返回值优化,因为该对象已命名。
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);
foo(Thing());
或者抛出异常并按值
抓住struct Thing{
Thing();
Thing(const Thing&);
};
void foo() {
Thing c;
throw c;
}
int main() {
try {
foo();
}
catch(Thing c) {
}
}
Common limitations of copy elision are:
大多数商业级编译器都支持copy elision&amp; (N)RVO(取决于优化设置)。
答案 3 :(得分:43)
复制elision是一种编译器优化技术,可消除不必要的复制/移动对象。
在以下情况下,允许编译器省略复制/移动操作,因此不能调用关联的构造函数:
#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
即使发生了复制省略并且没有调用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
。
现在,几乎所有编译器都在启用优化时提供复制省略(如果没有设置其他选项则禁用它)。
对于每个复制省略,省略了一个构造和一个匹配的复制销毁,从而节省了CPU时间,并且没有创建一个对象,从而节省了堆栈框架上的空间。
答案 4 :(得分:0)
在这里,我再举一个今天显然遇到的复制删除示例。
# 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