什么是副本和返回值优化?

发布于 2025-01-19 01:09:41 字数 376 浏览 2 评论 0原文

什么是复制省略?什么是(命名的)返回值优化?它们意味着什么?

它们会在什么情况下发生?有什么限制?

What is copy elision? What is (named) return value optimization? What do they imply?

In what situations can they occur? What are limitations?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

无法言说的痛 2025-01-26 01:09:41

简介

有关技术概述 - 跳至此答案

对于发生复制省略的常见情况 - 跳到此答案

复制省略是大多数编译器实现的一种优化,以防止在某些情况下出现额外的(可能昂贵的)复制。它使得按值返回或按值传递在实践中变得可行(有限制)。

这是唯一一种消除(哈!)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();
}

:设置中,以下输出均有效

世界你好!
制作了一份副本。
制作了一份副本。


世界你好!
已制作副本。


世界你好!

这也意味着可以创建更少的对象,因此您也不能依赖于调用的特定数量的析构函数。您不应该在复制/移动构造函数或析构函数中包含关键逻辑,因为您不能依赖它们的调用。

如果省略对复制或移动构造函数的调用,则该构造函数必须仍然存在并且必须可访问。这确保复制省略不允许复制通常不可复制的对象,例如因为它们具有私有或删除的复制/移动构造函数。

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
}

Introduction

For a technical overview - skip to this answer.

For common cases where copy elision occurs - skip to this answer.

Copy elision is an optimization implemented by most compilers to prevent extra (potentially expensive) copies in certain situations. It makes returning by value or pass-by-value feasible in practice (restrictions apply).

It's the only form of optimization that elides (ha!) the as-if rule - copy elision can be applied even if copying/moving the object has side-effects.

The following example taken from 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();
}

Depending on the compiler & settings, the following outputs are all valid:

Hello World!
A copy was made.
A copy was made.


Hello World!
A copy was made.


Hello World!

This also means fewer objects can be created, so you also can't rely on a specific number of destructors being called. You shouldn't have critical logic inside copy/move-constructors or destructors, as you can't rely on them being called.

If a call to a copy or move constructor is elided, that constructor must still exist and must be accessible. This ensures that copy elision does not allow copying objects which are not normally copyable, e.g. because they have a private or deleted copy/move constructor.

C++17: As of C++17, Copy Elision is guaranteed when an object is returned directly, and in this case, the copy or move constructor need not be accessible or present:

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
}
飘逸的'云 2025-01-26 01:09:41

的复制省略形式

技术概述

- 跳过此答案。对于不太技术的视图&amp;简介 - 跳过此答案

(命名)返回值优化是复制elision的一种常见形式。它指的是从方法返回值的对象将其副本浮出水面的情况。标准中列出的示例说明了命名返回值优化,因为该对象是命名的。

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) {  
  }             
}

复制省略的常见局限性为:

  • 多个返回点
  • 有条件的初始化

大多数商业级编译器支持副本Elision&amp; (n)RVO(取决于优化设置)。 C ++ 17使上述许多副本强制性的副本都必须进行。

Common forms of copy elision

For a technical overview - skip to this answer.

For a less technical view & introduction - skip to this answer.

(Named) Return value optimization is a common form of copy elision. It refers to the situation where an object returned by value from a method has its copy elided. The example set forth in the standard illustrates named return value optimization, since the object is named.

class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
Thing f() {
  Thing t;
  return t;
}
Thing t2 = f();

Regular return value optimization occurs when a temporary is returned:

class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
Thing f() {
  return Thing();
}
Thing t2 = f();

Other common places where copy elision takes place is when an object is constructed from a temporary:

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

or when an exception is thrown and caught by value:

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:

  • multiple return points
  • conditional initialization

Most commercial-grade compilers support copy elision & (N)RVO (depending on optimization settings). C++17 makes many of the above classes of copy elision mandatory.

垂暮老矣 2025-01-26 01:09:41

标准参考

技术视图较低的 &amp;简介 - 跳过此答案

对于发生复制责任的常见情况 - 跳过此答案

标准中定义

复制ELISION 在: 12.8复制和移动类对象

31)当满足某些标准时,允许实施省略班级的副本/移动构造
对象,即使对象的复制/移动构造函数和/或破坏者也具有副作用。在这种情况下,
该实现将省略复制/移动操作的源和目标视为两个不同的
参考同一对象的方式,该对象的破坏发生在时代的后期
当两个对象在没有优化的情况下被销毁时。 123 复制/移动的省略
在以下情况下允许操作,称为 copy Elision (可以合并到
消除多个副本):

- 在具有类返回类型的函数的返回语句中,当表达式为一个名称时
具有相同Cvunqualified
键入功能返回类型,可以通过构造省略复制/移动操作
自动对象直接进入函数的返回值

- 在抛出表达中,当操作数是非易失性自动对象的名称时(除了一个
函数或捕获条件参数)其范围不会超出最内向的末尾
封闭了try-block(如果有一个),将副本/移动操作从操作数到例外
可以通过将自动对象直接构造到异常对象

来省略对象(15.1)

- 当尚未绑定到参考(12.2)的临时类对象将被复制/移动
对于具有相同CV UNCALIFIFIFIFIFIFIFIFIFIFIFIFIEN类型的类对象,可以省略复制/移动操作
将临时对象直接构建到省略的副本/移动

的目标中

- 当异常处理程序的异常解释(第15条)声明同一类型的对象
(除CV合格外)作为异常对象(15.1),可以省略复制/移动操作
如果程序的含义
除了执行构造函数和破坏者对对象声明的对象的执行
异常 - 持续。

123),因为只有一个对象而不是两个对象,而一个副本/移动构造函数未执行,所以仍然有一个
每个构造的对象都被摧毁。

给定的示例是:

class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
Thing f() {
  Thing t;
  return t;
}
Thing t2 = f();

并解释:

在这里,可以将ELISION的标准组合在一起,以取消对类Thing> Thing的复制构造函数的两个调用:
将本地自动对象t复制到函数返回值f()的临时对象中
并将该临时对象复制到对象t2中。有效地,构建本地对象t
可以看作是直接初始化全局对象t2,该对象的破坏将发生在程序上
出口。向事物添加移动构造函数具有相同的效果,但这是从
t2的临时对象。

Standard reference

For a less technical view & introduction - skip to this answer.

For common cases where copy elision occurs - skip to this answer.

Copy elision is defined in the standard in:

12.8 Copying and moving class objects [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.

The example given is:

class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
Thing f() {
  Thing t;
  return t;
}
Thing t2 = f();

and explained:

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.

稀香 2025-01-26 01:09:41

复制省略是一种编译器优化技术,可消除不必要的对象复制/移动。

在以下情况下,允许编译器省略复制/移动操作,从而不调用关联的构造函数:

  1. NRVO(命名返回值优化):如果函数按值返回类类型,并且return 语句的表达式是具有自动存储持续时间的非易失性对象的名称(不是函数参数),那么可以省略非优化编译器将执行的复制/移动。如果是这样,则直接在存储中构造返回值,否则函数的返回值将被移动或复制到该存储中。
  2. 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  

即使复制省略发生并且复制/移动构造函数没有被调用,它必须存在并且可访问(就好像根本没有发生优化一样),否则程序是错误的。

您应该只在不会影响软件可观察行为的地方允许这种复制省略。复制省略是唯一允许具有(即省略)可观察到的副作用的优化形式。示例:

#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 时间,并且不会创建一个对象,从而节省堆栈帧上的空间。

Copy elision is a compiler optimization technique that eliminates unnecessary copying/moving of objects.

In the following circumstances, a compiler is allowed to omit copy/move operations and hence not to call the associated constructor:

  1. 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.
  2. 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  

Even when copy elision takes place and the copy-/move-constructor is not called, it must be present and accessible (as if no optimization happened at all), otherwise the program is ill-formed.

You should permit such copy elision only in places where it won’t affect the observable behavior of your software. Copy elision is the only form of optimization permitted to have (i.e. elide) observable side-effects. Example:

#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 provides the -fno-elide-constructors option to disable copy elision.
If you want to avoid possible copy elision, use -fno-elide-constructors.

Now almost all compilers provide copy elision when optimisation is enabled (and if no other option is set to disable it).

Conclusion

With each copy elision, one construction and one matching destruction of the copy are omitted, thus saving CPU time, and one object is not created, thus saving space on the stack frame.

痴骨ら 2025-01-26 01:09:41

这里我给出了我今天显然遇到的另一个复制省略的例子。

# 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

Here I give another example of copy elision that I apparently encountered today.

# 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";
  }

}

With the result:

In   Obj()
In   Obj()
assignment:
In   Obj(const Obj & org)
Obj_instance2.var1:3
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文