Answers:
有关技术概述,请跳至此答案。
复制省略是大多数编译器实现的一种优化,用于在某些情况下防止多余的副本(可能代价很高)。在实践中,这使按价值回报或按价值传递成为可行(有限制)。
这是唯一可以避免(ha!)优化规则的形式,即使复制/移动对象有副作用,也可以应用按规则复制省略。
以下示例摘自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();
}
根据编译器和设置,以下输出均有效:
你好,世界!
复制了。
复制了。
你好,世界!
复制了。
你好,世界!
这也意味着可以创建的对象更少,因此您也不能依赖于特定数量的析构函数的调用。复制/移动构造函数或析构函数内部不应具有批判逻辑,因为您不能依赖于它们的调用。
如果取消了对复制或移动构造函数的调用,则该构造函数必须仍然存在并且必须可访问。这确保了复制省略不允许复制通常不可复制的对象,例如,因为它们具有私有或已删除的复制/移动构造函数。
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
}
对于较不技术的观点和介绍,请跳至此答案。
标准中定义了复制省略:
如
31)当满足某些条件时,即使该对象的复制/移动构造函数和/或析构函数具有副作用,也允许实现忽略类对象的复制/移动构造。在这种情况下,实现将忽略的复制/移动操作的源和目标视为引用同一对象的两种不同方式,并且该对象的销毁发生在两个对象本来应该以较晚的时间发生。没有优化就销毁。123在以下情况下允许复制/移动操作的这种省略,称为复制清除(可以合并以消除多个副本):
—在具有类返回类型的函数的返回语句中,当表达式是具有与函数返回类型相同的cvunqualified类型的非易失性自动对象(函数或catch子句参数除外)的名称时,通过将自动对象直接构造到函数的返回值中,可以省略复制/移动操作
—在throw-expression中,当操作数是非易失性自动对象(函数或catch子句参数除外)的名称时,其范围不会超出最里面的try-block的末尾(如果存在)一种),通过将自动对象直接构造到异常对象中,可以省略从操作数到异常对象(15.1)的复制/移动操作
—当尚未绑定到引用(12.2)的临时类对象将被复制/移动到具有相同cv-unqualtype类型的类对象时,可以通过将临时对象直接构造为以下形式来省略复制/移动操作:省略复制/移动的目标
—当异常处理程序的异常声明(第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()
以及将该临时对象复制到object中t2
。实际上,t
可以将本地对象的构造视为直接初始化全局对象t2
,并且该对象的破坏将在程序退出时发生。在Thing中添加move构造函数具有相同的效果,但是从临时对象到其的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);
foo(Thing());
或抛出异常并按值捕获时:
struct Thing{
Thing();
Thing(const Thing&);
};
void foo() {
Thing c;
throw c;
}
int main() {
try {
foo();
}
catch(Thing c) {
}
}
大多数商业级编译器都支持复制省略和(N)RVO(取决于优化设置)。
复制省略是一种编译器优化技术,可消除不必要的对象复制/移动。
在以下情况下,允许编译器省略复制/移动操作,因此不调用关联的构造函数:
#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
GCC提供了-fno-elide-constructors
禁用复制删除功能的选项。如果要避免可能的复印省略,请使用-fno-elide-constructors
。
现在,几乎所有的编译器在启用优化时(如果未设置其他选项来禁用它)都提供复制省略。
每次删除副本时,都省略了副本的一种构造和一种匹配破坏,从而节省了CPU时间,并且不创建一个对象,从而节省了堆栈框架上的空间。
ABC obj2(xyz123());
是NRVO还是RVO?是否没有获得与ABC xyz = "Stack Overflow";//RVO