What are copy elision and return value optimization?
Asked Answered
G

5

536

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

In what situations can they occur? What are limitations?

Grievous answered 18/10, 2012 at 11:3 Comment(2)
Copy elision is one way to look at it; object elision or object fusion (or confusion) is another view.Sculpt
I found this link helpful.Millsaps
G
372

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
}
Grievous answered 18/10, 2012 at 11:3 Comment(16)
could you plz explain when is the 2nd output happen and when the 3rd?Surrebutter
@Surrebutter when and how the compiler decides to optimize that way.Grievous
then when the compiler decides the 2nd/3rd way? could you plz raise an example? thx~Surrebutter
@zhangxaochen, 1st output: copy 1 is from the return to a temp, and copy 2 from temp to obj; 2nd is when one of the above is optimezed, probably the reutnr copy is elided; the thris both are elidedLunch
@LuchianGrigore thanks for all those useful answers of yours! Could one state that on all major modern C++ compilers (GCC, Clang, MSVC++) in their latest versions, for each of them, the output of the example would be variant 3 which prints "Hello World!" only? Can we rely on that?Topazolite
@Topazolite I wouldn't know, don't use GCC or Clang that much. I'd assume with full optimizations on, yes, but I certainly wouldn't rely on it. That's the point.Grievous
Hmm, but in my opinion, this MUST be a feature we can rely on. Because if we can't, it would severely affect the way we implement our functions in modern C++ (RVO vs std::move). During watching some of the CppCon 2014 videos, i really got the impression that all modern compilers always do RVO. Furthermore, I've read somewhere that also without any optimizations on, the compilers apply it. But, of course, I am not sure about it. That's why I am asking.Topazolite
@j00hi: Never write move in a return statement - if rvo is not applied, the return value is moved out by default anyway.Saxena
Can you add a cout in constructor as well? And show all valid possibilities.Cupric
soo you say C++17 gurantees copy elison... does that mean that C++11 or C++0x code may have an extra copy?Malachi
C++17 section is a bit brief and slightly wrong: in C++17 it is no longer elision as there is no longer a temporary object to elide! Calling it guaranteed elision is a "lie to children" as far as I can tell.Filberto
@trevor yes; but modern compilers only do the copy when you disable elision as a compiler flag.Filberto
Also see https://mcmap.net/q/16541/-how-does-guaranteed-copy-elision-work/4454665 for guaranteed copy elisionPlanula
@Planula Is "Copy elision" a terminology?Squash
This answer would be improved if it added that "copy elision" is the technique used to implement the RVO/NRVO.Kudva
@JarrodSmith, I tried but the suggested edit queue is full so I couldn't edit.Riesman
G
133

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.

Grievous answered 18/10, 2012 at 11:4 Comment(2)
I'd be interested in seeing the "Common limitations" bullet points explained just a little bit... what makes these limiting factors?Commove
@Commove I linked against the msdn article, hope that clears some stuff out.Grievous
G
124

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.

Grievous answered 18/10, 2012 at 11:3 Comment(6)
Is that from the C++17 standard or from an earlier version?Euphroe
Why can't function parameter be return value optimized if it's the same type as function's return type?Auroora
This tries to answer - stackoverflow.com/questions/9444485/…Auroora
Is there any type of copy-elision for primitive types? If I have a function that propagates a return value (maybe an error code), will there be any optimisation similar to objects?Wreckful
"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." should add ",and the move construction from t to the temporary that that is elided". ? Or am I missing something?Multivocal
@Multivocal I think you are right. Both construction will use move ctor (assume Thing has a move ctor) without the copy elision.Enyedy
S
78

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.

Skiagraph answered 13/1, 2015 at 7:26 Comment(5)
the statement ABC obj2(xyz123()); is it NRVO or RVO? is it not getting temporary variable/object same as ABC xyz = "Stack Overflow";//RVO Archipenko
To have a more concrete illustration of RVO, you can refer to the assembly that the compiler generates (change the compiler flag -fno-elide-constructors to see the diff). godbolt.org/g/Y2KcdHAssignee
Isn't ABC xyz = "Stack Overflow"; just an implicit call to ABC::ABC(const char *ptr) rather than RVO?Crystallize
for ABC xyz = "Stack Overflow"; is calls explicitly defined copy constructor, so I am not sure how this can be RVO, and the word R- suggests that there is a return value from function, however constructors don't have return values.Stagey
More interestingly, ABC obj1( fun123()) this can be elided, however on C++17 and C++20 leave this as optional, so if -fno-elide-constructors is ON then def + move constructor will be called.Stagey
B
-1

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
Brycebryn answered 15/10, 2020 at 14:27 Comment(2)
That's already included in Luchian's answer (temporary object passed by value).Jenson
This answer can be a side note to the other answers. It shows an interesting extended case. In C++14, turning on -fno-elide-constructors gives 6 and without the option we get 3. In C++17 and above we get 3 always.Stealage

© 2022 - 2024 — McMap. All rights reserved.