Namespaces
Variants
Views
Actions

Difference between revisions of "cpp/language/copy elision"

From cppreference.com
< cpp‎ | language
(kill the note, I don't think anyone implemented that, and we also have a table for these already)
(Link update.)
 
(32 intermediate revisions by 12 users not shown)
Line 5: Line 5:
 
===Explanation===
 
===Explanation===
 
{{rrev|since=c++17|
 
{{rrev|since=c++17|
Under the following circumstances, the compilers are required to omit the copy and move construction of class objects, even if the copy/move constructor and the destructor have observable side-effects. The objects are constructed directly into the storage where they would otherwise be copied/moved to. The copy/move constructors need not be present or accessible, as the language rules ensure that no copy/move operation takes place, even conceptually:
+
====Prvalue semantics ("guaranteed copy elision")====
* In a {{rlp|return|return statement}}, when the operand is a {{rlp|value category|prvalue}} of the same class type (ignoring {{rlp|cv|cv-qualification}}) as the function return type:
+
Since C++17, a prvalue is not materialized until needed, and then it is constructed directly into the storage of its final destination. This sometimes means that even when the language syntax visually suggests a copy/move (e.g. {{rlp|copy initialization}}), no copy/move is performed — which means the type need not have an accessible copy/move constructor at all. Examples include:
 +
* Initializing the returned object in a {{rlp|return|return statement}}, when the operand is a {{rlp|value category|prvalue}} of the same class type (ignoring {{rlp|cv|cv-qualification}}) as the function return type:
 
{{source|1=
 
{{source|1=
T f() {
+
T f()
     return T();
+
{
 +
     return U(); // constructs a temporary of type U,
 +
                // then initializes the returned T from the temporary
 +
}
 +
T g()
 +
{
 +
    return T(); // constructs the returned T directly; no move
 
}
 
}
 
f(); // only one call to default constructor of T
 
 
}}
 
}}
* In the initialization of a variable, when the initializer expression is a {{rlp|value category|prvalue}} of the same class type (ignoring {{rlp|cv|cv-qualification}}) as the variable type:
+
: <!--CWG2426--> The destructor of the type returned must be accessible at the point of the return statement and non-deleted, even though no T object is destroyed.
 +
* In the initialization of an object, when the initializer expression is a {{rlp|value category|prvalue}} of the same class type (ignoring {{rlp|cv|cv-qualification}}) as the variable type:
 
{{source|1=
 
{{source|1=
T x = T(T(f())); // only one call to default constructor of T, to initialize x
+
T x = T(T(f())); // x is initialized by the result of f() directly; no move
 
}}
 
}}
 +
:This can only apply when the object being initialized is known not to be a potentially-overlapping subobject:
 +
{{source|
 +
struct C { /* ... */ };
 +
C f();
  
Note: the rule above does not specify an optimization: C++17 core language specification of {{rlp|value category|prvalues}} and {{rlp|implicit_conversion#Temporary_materialization|temporaries}} is fundamentally different from that of the earlier C++ revisions: there is no longer a temporary to copy/move from. Another way to describe C++17 mechanics is "unmaterialized value passing": prvalues are returned and used without ever materializing a temporary.
+
struct D;
 +
D g();
 +
 
 +
struct D : C
 +
{
 +
    D() : C(f()) {}   // no elision when initializing a base-class subobject
 +
    D(int) : D(g()) {} // no elision because the D object being initialized might
 +
                      // be a base-class subobject of some other class
 +
};
 
}}
 
}}
  
 +
Note: This rule does not specify an optimization, and the Standard does not formally describe it as "copy elision" (because nothing is being elided). Instead, the C++17 core language specification of {{rlp|value category|prvalues}} and {{rlp|implicit conversion#Temporary materialization|temporaries}} is fundamentally different from that of earlier C++ revisions: there is no longer a temporary to copy/move from. Another way to describe C++17 mechanics is "unmaterialized value passing" or "deferred temporary materialization": prvalues are returned and used without ever materializing a temporary.
 +
}}
 +
 +
====Non-mandatory copy{{rev inl|since=c++11|/move}} elision====
 
Under the following circumstances, the compilers are permitted, but not required to omit the copy{{rev inl|since=c++11| and move}} construction of class objects even if the copy{{rev inl|since=c++11|/move}} constructor and the destructor have observable side-effects. The objects are constructed directly into the storage where they would otherwise be copied/moved to. This is an optimization: even when it takes place and the copy{{rev inl|since=c++11|/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:
 
Under the following circumstances, the compilers are permitted, but not required to omit the copy{{rev inl|since=c++11| and move}} construction of class objects even if the copy{{rev inl|since=c++11|/move}} constructor and the destructor have observable side-effects. The objects are constructed directly into the storage where they would otherwise be copied/moved to. This is an optimization: even when it takes place and the copy{{rev inl|since=c++11|/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 {{rlp|return|return statement}}, when 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 which is of the same class type (ignoring {{rlp|cv|cv-qualification}}) as the function return type. This variant of copy elision is known as NRVO, "named return value optimization".
+
* In a {{rlp|return|{{c/core|return}} statement}}, when the operand is the name of a non-volatile object with automatic storage duration, which isn't a function parameter or a handler parameter, and which is of the same class type (ignoring {{rlp|cv|cv-qualification}}) as the function return type. This variant of copy elision is known as NRVO, "named return value optimization."
{{rev begin}}
+
{{rrev|until=c++17|
{{rev|until=c++17|
+
* In the initialization of an object, when the source object is a nameless temporary and is of the same class type (ignoring {{rlp|cv|cv-qualification}}) as the target object. When the nameless temporary is the operand of a return statement, this variant of copy elision is known as URVO, "unnamed return value optimization." (In C++17 and later, URVO is mandatory and no longer considered a form of copy elision; see above.)
* In the initialization of an object, when the source object is a nameless temporary and is of the same class type (ignoring {{rlp|cv|cv-qualification}}) as the target object. When the nameless temporary is the operand of a return statement, this variant of copy elision is known as RVO, "return value optimization".
+
 
}}
 
}}
{{rev|since=c++17|
 
Return value optimization is mandatory and no longer considered as copy elision; see above.
 
}}
 
{{rev end}}
 
 
{{rev begin}}
 
{{rev begin}}
 
{{rev|since=c++11|
 
{{rev|since=c++11|
* In a {{rlp|throw|throw-expression}}, when 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).
+
* In a {{rlp|throw|{{c/core|throw}} expression}}, when the operand is the name of a non-volatile object with automatic storage duration, which is not a function parameter or a handler parameter, and whose scope does not extend past the innermost {{rlp|try|{{c/core|try}} block}} (if exists).
* In a {{rlp|try_catch|catch clause}}, when the argument is of the same type (ignoring {{rlp|cv|cv-qualification}}) as the exception object thrown, the copy of the exception object is omitted and the body of the catch clause accesses the exception object directly, as if caught by reference (there cannot be a move from the exception object because it is always an lvalue). 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 argument (for example, if the catch clause argument is modified, and the exception object is rethrown with {{c|throw}}).
+
* In a {{rlp|catch|handler}}, when the argument is of the same type (ignoring {{rlp|cv|cv-qualification}}) as the exception object thrown, the copy of the {{rlpsd|throw#Exception object}} is omitted and the body of the handler accesses the exception object directly, as if caught by reference (there cannot be a move from the exception object because it is always an lvalue). 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 handler argument (for example, if the handler argument is modified, and the exception object is rethrown with {{c|throw}}).
 
}}
 
}}
 
{{rev|since=c++20|
 
{{rev|since=c++20|
Line 42: Line 59:
 
{{rev end}}
 
{{rev end}}
  
When copy elision occurs, the implementation treats the source and target of the omitted copy{{rev inl|since=c++11|/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 {{rev inl|since=c++17|(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)}}.
+
When copy elision occurs, the implementation treats the source and target of the omitted copy{{rev inl|since=c++11|/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 {{rev inl|since=c++11|(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)}}.
  
 
Multiple copy elisions may be chained to eliminate multiple copies.
 
Multiple copy elisions may be chained to eliminate multiple copies.
  
{{rrev|since=c++14|
+
{{rrev|since=c++11|
* In {{rlp|constant expression}} and {{rlp|constant initialization}}, return value optimization (RVO) is guaranteed<!-- CWG 2022 -->, however, named return value optimization (NRVO) is forbidden<!--CWG 2278-->:
+
* In {{rlp|constant expression}} and {{rlp|constant initialization}}, copy elision is never performed.<!-- CWG 2278, reversing the resolution of CWG 2022 -->
 
{{source|1=
 
{{source|1=
struct A {
+
struct A
     void *p;
+
{
     constexpr A(): p(this) {}
+
     void* p;
 +
     constexpr A() : p(this) {}
 +
    A(const A&); // Disable trivial copyability
 
};
 
};
  
constexpr A g()
+
constexpr A a; // OK: a.p points to a
{
+
    A a;
+
    return a;
+
}
+
  
constexpr A a;          // a.p points to a
+
constexpr A f()
// constexpr A b = g(); // error: b.p would be dangling and would point to a temporary
+
                        // with automatic storage duration
+
 
+
void g()
+
 
{
 
{
     A c = g();         // c.p may point to c or to an ephemeral temporary
+
     A x;
 +
    return x;
 
}
 
}
 +
constexpr A b = f(); // error: b.p would be dangling and point to the x inside f
  
extern const A d;
+
constexpr A c = A(); // (until C++17) error: c.p would be dangling and point to a temporary
constexpr A f()
+
                    // (since C++17) OK: c.p points to c; no temporary is involved
{
+
    A e;
+
    if (&e == &d)
+
        return A();
+
    else
+
        return e;
+
    // mandating NRVO in constant evaluation contexts would result in contradiction
+
    // that NRVO is performed if and only if it's not performed
+
}
+
// constexpr A d = f(); // error: d.p would be dangling
+
 
}}
 
}}
 
}}
 
}}
  
 
===Notes===
 
===Notes===
Copy elision is {{rev inl|until=c++14|the only allowed form of optimization}}{{rev inl|since=c++14|one of the two allowed forms of optimization, alongside {{rlp|new#Allocation|allocation elision and extension}},}} 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.
+
Copy elision is {{rev inl|until=c++14|the only allowed form of optimization}} {{rev inl|since=c++14|one of the two allowed forms of optimization, alongside {{rlp|new#Allocation|allocation elision and extension}},}} that can change 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.
  
 
{{rrev|since=c++11|
 
{{rrev|since=c++11|
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 {{rlp|return#Notes|return statement}} for details.
+
In a {{c/core|return}} statement or a {{c/core|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, {{rev inl|until=c++23|the compiler will attempt to use the move constructor even if the source operand is designated by an lvalue}} {{rev inl|since=c++23|the source operand will be treated as an rvalue}}; see {{rlp|return#Notes|{{c/core|return}} statement}} for details.
 
}}
 
}}
 +
{{feature test macro|std=C++17|value=201606L|__cpp_guaranteed_copy_elision|Guaranteed copy elision through simplified {{rlp|value category|value categories}}}}
  
 
===Example===
 
===Example===
 
{{example
 
{{example
|
+
|
| code=
+
|code=
 
#include <iostream>
 
#include <iostream>
#include <vector>
 
  
struct Noisy {
+
struct Noisy
     Noisy() { std::cout << "constructed\n"; }
+
{
 +
     Noisy() { std::cout << "constructed at " << this << '\n'; }
 
     Noisy(const Noisy&) { std::cout << "copy-constructed\n"; }
 
     Noisy(const Noisy&) { std::cout << "copy-constructed\n"; }
 
     Noisy(Noisy&&) { std::cout << "move-constructed\n"; }
 
     Noisy(Noisy&&) { std::cout << "move-constructed\n"; }
     ~Noisy() { std::cout << "destructed\n"; }
+
     ~Noisy() { std::cout << "destructed at " << this << '\n'; }
 
};
 
};
  
std::vector<Noisy> f() {
+
Noisy f()
     std::vector<Noisy> v = std::vector<Noisy>(3); // copy elision when initializing v
+
{
                                                  // from a temporary (until C++17)
+
     Noisy v = Noisy(); // (until C++17) copy elision initializing v from a temporary;
                                                  // from a prvalue (since C++17)
+
                      //              the move constructor may be called
     return v; // NRVO from v to the result object (not guaranteed, even in C++17)
+
                      // (since C++17) "guaranteed copy elision"
}            // if optimization is disabled, the move constructor is called
+
     return v; // copy elision ("NRVO") from v to the result object;
 +
              // the move constructor may be called
 +
}
  
void g(std::vector<Noisy> arg) {
+
void g(Noisy arg)
     std::cout << "arg.size() = " << arg.size() << '\n';
+
{
 +
     std::cout << "&arg = " << &arg << '\n';
 
}
 
}
  
int main() {
+
int main()
     std::vector<Noisy> v = f(); // copy elision in initialization of v
+
{
                                // from the temporary returned by f() (until C++17)
+
     Noisy v = f(); // (until C++17) copy elision initializing v from the result of f()
                                // from the prvalue f() (since C++17)
+
                  // (since C++17) "guaranteed copy elision"
     g(f());                     // copy elision in initialization of the parameter of g()
+
   
                                // from the temporary returned by f() (until C++17)
+
    std::cout << "&v = " << &v << '\n';
                                // from the prvalue f() (since C++17)
+
 
 +
     g(f()); // (until C++17) copy elision initializing arg from the result of f()
 +
            // (since C++17) "guaranteed copy elision"
 
}
 
}
| p=true
+
|p=true
| output=
+
|output=
constructed
+
constructed at 0x7fffd635fd4e
constructed
+
&v = 0x7fffd635fd4e
constructed
+
constructed at 0x7fffd635fd4f
constructed
+
&arg = 0x7fffd635fd4f
constructed
+
destructed at 0x7fffd635fd4f
constructed
+
destructed at 0x7fffd635fd4e
arg.size() = 3
+
destructed
+
destructed
+
destructed
+
destructed
+
destructed
+
destructed
+
 
}}
 
}}
 
  
 
===Defect reports===
 
===Defect reports===
 
{{dr list begin}}
 
{{dr list begin}}
{{dr list item|wg=cwg|dr=2022|std=C++14|before=copy elision was optional in constant expressions|after=copy elision mandatory}}
+
{{dr list item|wg=cwg|dr=1967|std=C++11|before=when copy elision is done using a move constructor, the<br>lifetime of the moved-from object was still considered|after=not considered}}
{{dr list item|wg=cwg|dr=2278|std=C++14|before=NRVO was mandatory in constant expressions|after=forbid NRVO in constant expressions}}
+
{{dr list item|wg=cwg|dr=2022|std=C++11|before=copy elision was optional during constant evaluation|after=mandatory during constant evaluation}}
 +
{{dr list item|wg=cwg|dr=2278|std=C++11|before=copy elision was mandatory during constant evaluation|after=forbidden during constant evaluation}}
 +
{{dr list item|wg=cwg|dr=2426|std=C++17|before=destructor not required when returning a prvalue|after=destructor is potentially invoked}}
 
{{dr list end}}
 
{{dr list end}}
  

Latest revision as of 00:36, 6 June 2024

 
 
C++ language
General topics
Flow control
Conditional execution statements
if
Iteration statements (loops)
for
range-for (C++11)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications (until C++17*)
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
const/volatile
decltype (C++11)
auto (C++11)
constexpr (C++11)
consteval (C++20)
constinit (C++20)
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
Memory allocation
Classes
Class-specific function properties
explicit (C++11)
static

Special member functions
Templates
Miscellaneous
 
 

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

Contents

[edit] Explanation

Prvalue semantics ("guaranteed copy elision")

Since C++17, a prvalue is not materialized until needed, and then it is constructed directly into the storage of its final destination. This sometimes means that even when the language syntax visually suggests a copy/move (e.g. copy initialization), no copy/move is performed — which means the type need not have an accessible copy/move constructor at all. Examples include:

T f()
{
    return U(); // constructs a temporary of type U,
                // then initializes the returned T from the temporary
}
T g()
{
    return T(); // constructs the returned T directly; no move
}
The destructor of the type returned must be accessible at the point of the return statement and non-deleted, even though no T object is destroyed.
  • In the initialization of an object, when the initializer expression is a prvalue of the same class type (ignoring cv-qualification) as the variable type:
T x = T(T(f())); // x is initialized by the result of f() directly; no move
This can only apply when the object being initialized is known not to be a potentially-overlapping subobject:
struct C { /* ... */ };
C f();
 
struct D;
D g();
 
struct D : C
{
    D() : C(f()) {}    // no elision when initializing a base-class subobject
    D(int) : D(g()) {} // no elision because the D object being initialized might
                       // be a base-class subobject of some other class
};

Note: This rule does not specify an optimization, and the Standard does not formally describe it as "copy elision" (because nothing is being elided). Instead, the C++17 core language specification of prvalues and temporaries is fundamentally different from that of earlier C++ revisions: there is no longer a temporary to copy/move from. Another way to describe C++17 mechanics is "unmaterialized value passing" or "deferred temporary materialization": prvalues are returned and used without ever materializing a temporary.

(since C++17)

[edit] Non-mandatory copy/move(since C++11) elision

Under the following circumstances, the compilers are permitted, but not required to omit the copy and move(since C++11) construction of class objects even if the copy/move(since C++11) constructor and the destructor have observable side-effects. The objects are constructed directly into the storage where they would otherwise be copied/moved to. This is an optimization: even when it takes place and the copy/move(since C++11) 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, when the operand is the name of a non-volatile object with automatic storage duration, which isn't a function parameter or a handler parameter, and which is of the same class type (ignoring cv-qualification) as the function return type. This variant of copy elision is known as NRVO, "named return value optimization."
  • In the initialization of an object, when the source object is a nameless temporary and is of the same class type (ignoring cv-qualification) as the target object. When the nameless temporary is the operand of a return statement, this variant of copy elision is known as URVO, "unnamed return value optimization." (In C++17 and later, URVO is mandatory and no longer considered a form of copy elision; see above.)
(until C++17)
  • In a throw expression, when the operand is the name of a non-volatile object with automatic storage duration, which is not a function parameter or a handler parameter, and whose scope does not extend past the innermost try block (if exists).
  • In a handler, when the argument is of the same type (ignoring cv-qualification) as the exception object thrown, the copy of the exception object is omitted and the body of the handler accesses the exception object directly, as if caught by reference (there cannot be a move from the exception object because it is always an lvalue). 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 handler argument (for example, if the handler argument is modified, and the exception object is rethrown with throw).
(since C++11)
  • In coroutines, copy/move of the parameter into coroutine state may be elided where this does not change the behavior of the program other than by omitting the calls to the parameter's constructor and destructor. This can take place if the parameter is never referenced after a suspension point or when the entire coroutine state was never heap-allocated in the first place.
(since C++20)

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++11).

Multiple copy elisions may be chained to eliminate multiple copies.

struct A
{
    void* p;
    constexpr A() : p(this) {}
    A(const A&); // Disable trivial copyability
};
 
constexpr A a;  // OK: a.p points to a
 
constexpr A f()
{
    A x;
    return x;
}
constexpr A b = f(); // error: b.p would be dangling and point to the x inside f
 
constexpr A c = A(); // (until C++17) error: c.p would be dangling and point to a temporary
                     // (since C++17) OK: c.p points to c; no temporary is involved
(since C++11)

[edit] 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 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.

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 source operand is designated by an lvalue(until C++23) the source operand will be treated as an rvalue(since C++23); see return statement for details.

(since C++11)
Feature-test macro Value Std Feature
__cpp_guaranteed_copy_elision 201606L (C++17) Guaranteed copy elision through simplified value categories

[edit] Example

#include <iostream>
 
struct Noisy
{
    Noisy() { std::cout << "constructed at " << this << '\n'; }
    Noisy(const Noisy&) { std::cout << "copy-constructed\n"; }
    Noisy(Noisy&&) { std::cout << "move-constructed\n"; }
    ~Noisy() { std::cout << "destructed at " << this << '\n'; }
};
 
Noisy f()
{
    Noisy v = Noisy(); // (until C++17) copy elision initializing v from a temporary;
                       //               the move constructor may be called
                       // (since C++17) "guaranteed copy elision"
    return v; // copy elision ("NRVO") from v to the result object;
              // the move constructor may be called
}
 
void g(Noisy arg)
{
    std::cout << "&arg = " << &arg << '\n';
}
 
int main()
{
    Noisy v = f(); // (until C++17) copy elision initializing v from the result of f()
                   // (since C++17) "guaranteed copy elision"
 
    std::cout << "&v = " << &v << '\n';
 
    g(f()); // (until C++17) copy elision initializing arg from the result of f()
            // (since C++17) "guaranteed copy elision"
}

Possible output:

constructed at 0x7fffd635fd4e
&v = 0x7fffd635fd4e
constructed at 0x7fffd635fd4f
&arg = 0x7fffd635fd4f
destructed at 0x7fffd635fd4f
destructed at 0x7fffd635fd4e

[edit] 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 1967 C++11 when copy elision is done using a move constructor, the
lifetime of the moved-from object was still considered
not considered
CWG 2022 C++11 copy elision was optional during constant evaluation mandatory during constant evaluation
CWG 2278 C++11 copy elision was mandatory during constant evaluation forbidden during constant evaluation
CWG 2426 C++17 destructor not required when returning a prvalue destructor is potentially invoked

[edit] See also