什么是复制省略?什么是(命名)返回值优化?它们意味着什么?
在什么情况下会发生?什么是限制?
如果你提到了这个问题,你可能正在寻找介绍。 有关技术概述,请参阅标准参考。 常见情况见这里。
什么是复制省略?什么是(命名)返回值优化?它们意味着什么?
在什么情况下会发生?什么是限制?
如果你提到了这个问题,你可能正在寻找介绍。 有关技术概述,请参阅标准参考。 常见情况见这里。
当前回答
复制省略是一种编译器优化技术,可以消除不必要的复制/移动对象。
在以下情况下,编译器允许省略复制/移动操作,因此不调用相关的构造函数:
NRVO (Named Return Value Optimization): If a function returns a class type by value and the return statement's expression is the name of a non-volatile object with automatic storage duration (which isn't a function parameter), then the copy/move that would be performed by a non-optimising compiler can be omitted. If so, the returned value is constructed directly in the storage to which the function's return value would otherwise be moved or copied. RVO (Return Value Optimization): If the function returns a nameless temporary object that would be moved or copied into the destination by a naive compiler, the copy or move can be omitted as per 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-省略-constructors选项来禁用复制省略。 如果你想避免可能的复制省略,请使用-fno-elide-constructors。
现在几乎所有的编译器都在启用优化时提供拷贝省略(如果没有其他选项设置为禁用它)。
结论
对于每个副本省略,省略了副本的一次构造和一次匹配的销毁,从而节省了CPU时间,并且不创建一个对象,从而节省了堆栈帧上的空间。
其他回答
复制省略是一种编译器优化技术,可以消除不必要的复制/移动对象。
在以下情况下,编译器允许省略复制/移动操作,因此不调用相关的构造函数:
NRVO (Named Return Value Optimization): If a function returns a class type by value and the return statement's expression is the name of a non-volatile object with automatic storage duration (which isn't a function parameter), then the copy/move that would be performed by a non-optimising compiler can be omitted. If so, the returned value is constructed directly in the storage to which the function's return value would otherwise be moved or copied. RVO (Return Value Optimization): If the function returns a nameless temporary object that would be moved or copied into the destination by a naive compiler, the copy or move can be omitted as per 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-省略-constructors选项来禁用复制省略。 如果你想避免可能的复制省略,请使用-fno-elide-constructors。
现在几乎所有的编译器都在启用优化时提供拷贝省略(如果没有其他选项设置为禁用它)。
结论
对于每个副本省略,省略了副本的一次构造和一次匹配的销毁,从而节省了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
简介
有关技术概述,请跳过此回答。
对于发生复制省略的常见情况,请跳过此答案。
复制省略是大多数编译器实现的一种优化,用于在某些情况下防止额外的(可能昂贵的)复制。它使得按值返回或按值传递在实践中是可行的(适用限制)。
这是省略(哈!)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();
}
根据编译器和设置,以下输出都是有效的:
你好世界! 复印了一份。 复印了一份。
你好世界! 复印了一份。
你好世界!
这也意味着可以创建更少的对象,因此也不能依赖于调用特定数量的析构函数。你不应该在复制/移动构造函数或析构函数中包含关键逻辑,因为你不能依赖于它们被调用。
如果省略了对复制或移动构造函数的调用,则该构造函数必须仍然存在并且必须是可访问的。这确保了复制省略不允许复制通常不可复制的对象,例如,因为它们有一个私有或已删除的复制/移动构造函数。
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
}
常见的复制省略形式
有关技术概述,请跳过此回答。
对于一个不那么技术性的观点和介绍-跳过这个答案。
(命名)返回值优化是复制省略的一种常见形式。它指的是方法通过值返回的对象的副本被省略的情况。标准中给出的示例说明了命名返回值优化,因为对象是命名的。
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强制执行了上述许多复制省略类。
标准参考
对于一个不那么技术性的观点和介绍-跳过这个答案。
对于发生复制省略的常见情况,请跳过此答案。
复制省略在标准中定义:
12.8复制和移动类对象[class.copy]
as
31) When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the copy/move constructor and/or destructor for the object have side effects. In such cases, the implementation treats the source and target of the omitted copy/move operation as simply two different ways of referring to the same object, and the destruction of that object occurs at the later of the times when the two objects would have been destroyed without the optimization.123 This elision of copy/move operations, called copy elision, is permitted in the following circumstances (which may be combined to eliminate multiple copies): — in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) with the same cvunqualified type as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function’s return value — in a throw-expression, when the operand is the name of a non-volatile automatic object (other than a function or catch-clause parameter) whose scope does not extend beyond the end of the innermost enclosing try-block (if there is one), the copy/move operation from the operand to the exception object (15.1) can be omitted by constructing the automatic object directly into the exception object — when a temporary class object that has not been bound to a reference (12.2) would be copied/moved to a class object with the same cv-unqualified type, the copy/move operation can be omitted by constructing the temporary object directly into the target of the omitted copy/move — when the exception-declaration of an exception handler (Clause 15) declares an object of the same type (except for cv-qualification) as the exception object (15.1), the copy/move operation can be omitted by treating the exception-declaration as an alias for the exception object if the meaning of the program will be unchanged except for the execution of constructors and destructors for the object declared by the exception-declaration. 123) Because only one object is destroyed instead of two, and one copy/move constructor is not executed, there is still one object destroyed for each one constructed.
给出的例子是:
class Thing {
public:
Thing();
~Thing();
Thing(const Thing&);
};
Thing f() {
Thing t;
return t;
}
Thing t2 = f();
和解释:
Here the criteria for elision can be combined to eliminate two calls to the copy constructor of class Thing: the copying of the local automatic object t into the temporary object for the return value of function f() and the copying of that temporary object into object t2. Effectively, the construction of the local object t can be viewed as directly initializing the global object t2, and that object’s destruction will occur at program exit. Adding a move constructor to Thing has the same effect, but it is the move construction from the temporary object to t2 that is elided.