1. Changelog
-
R3: Eliminate discussion and move to CWG wording review.
-
R2: Added discussion of
.return ++ x -
R1: Added discussion of
.return x += y
2. Background
Each version of C++ has improved the efficiency of returning objects by value. By the middle of the last decade, copy elision was reliable (if not technically guaranteed) in situations like this:
Widget one () { return Widget (); // copy elision } Widget two () { Widget result ; return result ; // copy elision }
In C++11, a completely new feature was added: a change to overload resolution which I will call implicit move. Even when copy elision is impossible, the compiler is sometimes
required to implicitly move the
statement’s operand into the result object:
std :: shared_ptr < Base > three () { std :: shared_ptr < Base > result ; return result ; // copy elision } std :: shared_ptr < Base > four () { std :: shared_ptr < Derived > result ; return result ; // no copy elision, but implicitly moved (not copied) }
The wording for this optimization was amended by [CWG1579]. N4762’s wording in [class.copy.elision]/3 says:
In the following copy-initialization contexts, a move operation might be used instead of a copy operation:
If the expression in a
statement is a (possibly parenthesized) id-expression that names an object with automatic storage duration declared in the body or parameter-declaration-clause of the innermost enclosing function or lambda-expression, or
return if the operand of a throw-expression 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),overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue. If the first overload resolution fails or was not performed, or if the type of the first parameter of the selected
constructor is not anrvalue reference tothe object’s type (possibly cv-qualified), overload resolution is performed again, considering the object as an lvalue.
Note: Post-Kona draft N4810 extends this wording to cover
statements too. I don’t
understand coroutines very well, but I don’t know of any reason why P1155’s changes should not apply to
just as well as to
and
. The proposed wording below reflects my beliefs.
The highlighted phrases above indicate places where the wording diverges from a naïve programmer’s intuition. Consider the following examples...
2.1. Throwing is pessimized
Throwing is pessimized because of the highlighted word
void five () { Widget w ; throw w ; // non-guaranteed copy elision, but implicitly moved (never copied) } Widget six ( Widget w ) { return w ; // no copy elision, but implicitly moved (never copied) } void seven ( Widget w ) { throw w ; // no copy elision, and no implicit move (the object is copied) }
Note: The comment in
matches the current Standard wording, and matches the behavior of GCC.
Most compilers (Clang 4.0.1+, MSVC 2015+, ICC 16.0.3+) already do this implicit move.
2.2. Non-constructor conversion is pessimized
Non-constructor conversion is pessimized because of the highlighted word
struct From { From ( Widget const & ); From ( Widget && ); }; struct To { operator Widget () const & ; operator Widget () && ; }; From eight () { Widget w ; return w ; // no copy elision, but implicitly moved (never copied) } Widget nine () { To t ; return t ; // no copy elision, and no implicit move (the object is copied) }
2.3. By-value sinks are pessimized
By-value sinks are pessimized because of the highlighted phrase
struct Fish { Fish ( Widget const & ); Fish ( Widget && ); }; struct Fowl { Fowl ( Widget ); }; Fish ten () { Widget w ; return w ; // no copy elision, but implicitly moved (never copied) } Fowl eleven () { Widget w ; return w ; // no copy elision, and no implicit move (the Widget object is copied) }
Note: The comment in
matches the current Standard wording, and matches the behavior of
Clang, ICC, and MSVC. One compiler (GCC 5.1+) already does this implicit move.
2.4. Slicing is pessimized
Slicing is pessimized because of the highlighted phrase
std :: shared_ptr < Base > twelve () { std :: shared_ptr < Derived > result ; return result ; // no copy elision, but implicitly moved (never copied) } Base thirteen () { Derived result ; return result ; // no copy elision, and no implicit move (the object is copied) }
Note: The comment in
matches the current Standard wording, and matches the behavior
of Clang and MSVC. Some compilers (GCC 8.1+, ICC 18.0.0+) already do this implicit move.
We propose to remove all four of these unnecessary limitations.
3. Proposed wording relative to N4810
Modify [class.copy.elision]/3 as follows:
In the following copy-initialization contexts, a move operation might be used instead of a copy operation:
If the expression in a
or
return statement is a (possibly parenthesized) id-expression that names an object with automatic storage duration declared in the body or parameter-declaration-clause of the innermost enclosing function or lambda-expression, or
co_return if the operand of a throw-expression is the name of a non-volatile automatic object (other than a
function orcatch-clause parameter) whose scope does not extend beyond the end of the innermost enclosing try-block (if there is one),overload resolution to select the constructor for the copy or the
overload to call is first performed as if the object were designated by an rvalue. If the first overload resolution fails or was not performed,
return_value or if the type of the first parameter of the selected constructor or theoverload resolution is performed again, considering the object as an lvalue. [Note: This two-stage overload resolution must be performed regardless of whether copy elision will occur. It determines the constructor or theoverload is not an rvalue reference to the object’s type (possibly cv-qualified),
return_value overload to be called if elision is not performed, and the selected constructor or
return_value theoverload must be accessible even if the call is elided. —end note]
return_value