Copy elision

From cppreference.com
< cpp‎ | language
 
 
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function declaration
inline specifier
Exception specifications (deprecated)
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
decltype (C++11)
auto (C++11)
alignas (C++11)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Implicit conversions - Explicit conversions
static_cast - dynamic_cast
const_cast - reinterpret_cast
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous
 

Optimizes out copy- and move- (since C++11)constructors, resulting in zero-copy pass-by-value semantics.

Explanation

Under the following circumstances, the compilers are required to omit the copy- and move- constructors of class objects even if copy/move constructor and the destructor have observable side-effects:

  • In initialization, if the initializer expression is a prvalue and the cv-unqualified version of the source type is the same class as the class of the destination, the initializer expression is used to initialize the destination object:
T x = T(T(T())); // only one call to default constructor of T, to initialize x
  • In a function call, if the operand of a return statement is a prvalue and the return type of the function is the same as the type of that prvalue.
T f() { return T{}; }
T x = f();         // only one call to default constructor of T, to initialize x
T* p = new T(f()); // only one call to default constructor of T, to initialize *p
(since C++17)

Under the following circumstances, the compilers are permitted to omit the copy- and move- (since C++11)constructors of class objects even if copy/move (since C++11) constructor and the destructor have observable side-effects.

  • 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 the function parameter, or a catch clause parameter, and which has the same type (ignoring top-level cv-qualification) as the return type of the function, then copy/move (since C++11) is omitted. When that local object is constructed, it is constructed directly in the storage where the function's return value would otherwise be moved or copied to. This variant of copy elision is known as NRVO, "named return value optimization".
  • When a nameless temporary, not bound to any references, would be moved or (since C++11) copied into an object of the same type (ignoring top-level cv-qualification), the copy/move (since C++11) is omitted. When that temporary is constructed, it is constructed directly in the storage where it would otherwise be moved or (since C++11) copied to. When the nameless temporary is the argument of a return statement, this variant of copy elision is known as RVO, "return value optimization".
(until C++17)

This optimization is mandatory; see above.

(since C++17)
  • In a throw-expression, if the operand is the name of a non-volatile object with automatic storage duration, which isn't a function parameter or a catch clause parameter, and whose scope does not extend past the innermost try-block (if there is a try-block), then copy/move is omitted. When that local object is constructed, it is constructed directly in the storage where the exception object would otherwise be moved or copied to.
  • When handling an exception, if the argument of the catch clause is of the same type (ignoring top-level cv-qualification) as the exception object thrown, the copy is omitted and the body of the catch clause accesses the exception object directly, as if caught by reference. This is disabled if such copy elision would change the observable behavior of the program for any reason other than skipping the copy constructor and the destructor of the catch clause's argument (for example, if the catch clause argument is modified, and the exception object is rethrown with throw;).
(since C++11)

When copy elision occurs, the implementation treats the source and target of the omitted copy/move (since C++11) 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 (except that, if the parameter of the selected constructor is an rvalue reference to object type, the destruction occurs when the target would have been destroyed) (since C++17).

Multiple copy elisions may be chained to eliminate multiple copies.

struct A {
    void *p;
    constexpr A(): p(this) {}
};
 
constexpr A g() {
  A a;
  return a;
}
 
constexpr A a;        // a.p points to a
constexpr A b = g();  // b.p points to b (NRVO guaranteed)
 
void g() {
  A c = g();          // c.p may point to c or to an ephemeral temporary
}
(since C++14)

Notes

Copy elision is the only allowed form of optimization (until C++14)one of the two allowed forms of optimization, alongside allocation elision and extension, (since C++14) that can change the observable side-effects. Because some compilers do not perform copy elision in every situation where it is allowed (e.g., in debug mode), programs that rely on the side-effects of copy/move constructors and destructors are not portable.

When copy-elision takes place (until C++17)In those cases where copy-elision is not guaranteed, if it takes place (since C++17) and the copy-/move-constructor is not called, it still must be present and accessible (as if no optimization happened at all), otherwise the program is ill-formed.

In a return statement or a throw-expression, if the compiler cannot perform copy elision but the conditions for copy elision are met or would be met, except that the source is a function parameter, the compiler will attempt to use the move constructor even if the object is designated by an lvalue; see return statement for details.

(since C++11)

Example

#include <iostream>
#include <vector>
 
struct Noisy
{
    Noisy() { std::cout << "constructed\n"; }
    Noisy(const Noisy&) { std::cout << "copy-constructed\n"; }
    Noisy(Noisy&&) { std::cout << "move-constructed\n"; }
    ~Noisy() { std::cout << "destructed\n"; }
};
 
std::vector<Noisy> f()
{
    std::vector<Noisy> v = std::vector<Noisy>(3); // copy elision when initializing
                                                  // v from a temporary
                                                  // (guaranteed in C++17)
    return v; // NRVO from v to the returned nameless temporary (not guaranteed in C++17)
}             // or the move constructor is called if optimizations are disabled
 
void g(std::vector<Noisy> arg)
{
    std::cout << "arg.size() = " << arg.size() << '\n';
}
 
int main()
{
    std::vector<Noisy> v = f(); // copy elision in initialization of v
                                // from the result of f() (guaranteed in C++17)
    g(f());                     // copy elision in initialization of the 
                                // parameter of g() from the result of f()
                                // (guaranteed in C++17)
}

Possible output:

constructed
constructed
constructed
constructed
constructed
constructed
arg.size() = 3
destructed
destructed
destructed
destructed
destructed
destructed


Defect reports

The following behavior-changing defect reports were applied retroactively to previously published C++ standards.

DR Applied to Behavior as published Correct behavior
CWG 2022 C++14 copy elision was optional in constant expressions copy elision mandatory

See also