How Does Guaranteed Copy Elision Work

How does guaranteed copy elision work?

Copy elision was permitted to happen under a number of circumstances. However, even if it was permitted, the code still had to be able to work as if the copy were not elided. Namely, there had to be an accessible copy and/or move constructor.

Guaranteed copy elision redefines a number of C++ concepts, such that certain circumstances where copies/moves could be elided don't actually provoke a copy/move at all. The compiler isn't eliding a copy; the standard says that no such copying could ever happen.

Consider this function:

T Func() {return T();}

Under non-guaranteed copy elision rules, this will create a temporary, then move from that temporary into the function's return value. That move operation may be elided, but T must still have an accessible move constructor even if it is never used.

Similarly:

T t = Func();

This is copy initialization of t. This will copy initialize t with the return value of Func. However, T still has to have a move constructor, even though it will not be called.

Guaranteed copy elision redefines the meaning of a prvalue expression. Pre-C++17, prvalues are temporary objects. In C++17, a prvalue expression is merely something which can materialize a temporary, but it isn't a temporary yet.

If you use a prvalue to initialize an object of the prvalue's type, then no temporary is materialized. When you do return T();, this initializes the return value of the function via a prvalue. Since that function returns T, no temporary is created; the initialization of the prvalue simply directly initilaizes the return value.

The thing to understand is that, since the return value is a prvalue, it is not an object yet. It is merely an initializer for an object, just like T() is.

When you do T t = Func();, the prvalue of the return value directly initializes the object t; there is no "create a temporary and copy/move" stage. Since Func()'s return value is a prvalue equivalent to T(), t is directly initialized by T(), exactly as if you had done T t = T().

If a prvalue is used in any other way, the prvalue will materialize a temporary object, which will be used in that expression (or discarded if there is no expression). So if you did const T &rt = Func();, the prvalue would materialize a temporary (using T() as the initializer), whose reference would be stored in rt, along with the usual temporary lifetime extension stuff.

One thing guaranteed elision permits you to do is return objects which are immobile. For example, lock_guard cannot be copied or moved, so you couldn't have a function that returned it by value. But with guaranteed copy elision, you can.

Guaranteed elision also works with direct initialization:

new T(FactoryFunction());

If FactoryFunction returns T by value, this expression will not copy the return value into the allocated memory. It will instead allocate memory and use the allocated memory as the return value memory for the function call directly.

So factory functions that return by value can directly initialize heap allocated memory without even knowing about it. So long as these function internally follow the rules of guaranteed copy elision, of course. They have to return a prvalue of type T.

Of course, this works too:

new auto(FactoryFunction());

In case you don't like writing typenames.


It is important to recognize that the above guarantees only work for prvalues. That is, you get no guarantee when returning a named variable:

T Func()
{
T t = ...;
...
return t;
}

In this instance, t must still have an accessible copy/move constructor. Yes, the compiler can choose to optimize away the copy/move. But the compiler must still verify the existence of an accessible copy/move constructor.

So nothing changes for named return value optimization (NRVO).

Guaranteed copy elision and deleted copy/move constructor when throwing an exception

Your understanding of the standard is correct, in so far as your analysis of that paragraph actually applies.

But it doesn't apply, because no constructor is ever considered for copy-initialization.

Guaranteed elision is something of a misnomer; it's a useful explanation of the concept, but it doesn't reflect exactly how it works as far as the standard is concerned. Guaranteed elision works by rewriting the meaning of prvalues so there never is a copy/move to be elided in the first place.

A a = A{}; is copy-initialization, but it doesn't even hypothetically call a copy/move constructor. The variable a is initialized by the prvalue's initializer:

The result of a prvalue is the value that the expression stores into its context. A prvalue whose result is the value V is sometimes said to have or name the value V. The result object of a prvalue is the object initialized by the prvalue

a is the "result object" initialized by the prvalue.

The same goes here. A{} is a prvalue. The exception object is the "result object" to be initialized by the prvalue. There's no temporary object, no copy/move constructors that ever get considered for usage.

With guaranteed copy elision, why does the class need to be fully defined?

The rule lies in [basic.lval]/9:

Unless otherwise indicated ([dcl.type.simple]), a prvalue shall always have complete type or the void type; ...

Does guaranteed copy elision work with function parameters?

In C++17, prvalues ("anonymous temporaries") are no longer objects. Instead, they are instructions on how to construct an object.

They can instantiate a temporary from their construction instructions, but as there is no object there, there is no copy/move construction to elide.

Foo myfunc(Foo foo) {
return foo;
}

So here, the function argument foo is moved into the prvalue return value of myfunc. You can think of this conceptually as "myfunc returns instructions on how to make a Foo". If those instructions are "not used" by your program, a temporary is automatically instantiated and uses those instructions.

(The instructions, by the way, include time travel provisions; the time-point of construction remains inside the function itself!)

auto foo = myfunc(Foo());

So here, Foo() is a prvalue. It says "construct a Foo using the () constructor". It is then used to construct the argument of myfunc. No elision occurs, no copy constructor or move constructor is called, just ().

Stuff then happens inside myfunc.

myfunc returns a prvalue of type Foo. This prvalue (aka construction instructions) is used to construct the local variable auto foo.

So what happens here is that a Foo is constructed via (), then moved into auto foo.

Elision of function arguments into return values is not supported in C++14 nor C++17 as far as I know (I could be wrong, I do not have chapter and verse of the standard here). However, they are implicitly moved when used in a return func_arg; context.

How does guaranteed copy elision work in list-initialization in C++1z?

Guaranteed elision works by redefining prvalue expressions to mean "will initialize an object". They don't construct temporaries anymore; temporaries are instead constructed by certain uses of prvalue expressions.

Please note the frequent use of the word "expression" above. I point that out because of one very important fact: a braced-init-list is not an expression. The standard is very clear about this. It is not an expression, and only expressions can be prvalues.

Indeed, consider the section of the standard on elision:

This elision of copy/move operations, called copy elision, is permitted in the following circumstances:

  • in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic 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

These all involve expressions (temporary class objects are expressions). Braced-init-lists aren't expressions.

As such, if you issue return {anything};, the construction of the return value from anything will not be elided, regardless of what anything is. According to the standard, of course; compilers may differ due to bugs.

Now that being said, if you have a prvalue expression of the same type as the return value, you are highly unlikely to want to type return {prvalue}; instead of just return prvalue;. And if the expression was of a different type, then it doesn't qualify for elision anyway.

Shouldn't guaranteed copy elision apply?

From https://en.cppreference.com/w/cpp/language/copy_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. This is an optimization: even when it takes place 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.

  • 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, 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".

NRVO is not guaranteed to happen, but would be possible in your case.

As pointed out by Sander De Dycker in a comment,
there is already a bug report to get this opportunity of elision.

Does the behavior of guaranteed copy elision depend on existence of user-defined copy constructor?

Quoting from C++17 Working Draft §15.2 Temporary Objects Paragraph 3 (https://timsong-cpp.github.io/cppwp/class.temporary#3):

When an object of class type X is passed to or returned from a function, if each copy constructor, move constructor, and destructor of X is either trivial or deleted, and X has at least one non-deleted copy or move constructor, implementations are permitted to create a temporary object to hold the function parameter or result object. ... [ Note: This latitude is granted to allow objects of class type to be passed to or returned from functions in registers. — end note]

In your case, when I made both copy and move constructors defaulted:

S(const S &) = default;
S(S &&) = default;

assertion failed as well with GCC and Clang. Note that implicitly-defined constructors are trivial.

Guaranteed copy elision and forward declaration of returned type

Even with "guaranteed copy elision" (which, unfortunately, is a bit of a misnomer), the C++ standard requires [dcl.fct]/11 that the return type of a function

[…] shall not be an incomplete (possibly cv-qualified) class type in the context of the function definition unless the function is deleted.

Using a placeholder type for the return type in your function definition (as was also suggested by Max Langhof in the comments) should work around the problem in this case:

template <typename Derived>
struct Interface
{
auto f() const { return static_cast<const Derived&>(*this).f_impl(); }
};

working example here

Note that "guaranteed copy elision" really is not a guarantee that a copy will be elided as much as it is a change to the language that means that a copy is never made in the first place. Without there being a copy, there is also nothing to elide…

Not making a copy means the compiler has to construct an object directly in the destination of the return value instead. And doing so requires a complete type…

Guaranteed copy elision for uniform braced array initialization - Shouldn't this be mandatory since C++17? [duplicate]

This should also work pre-C++17, where this syntax is direct-initialization. See How to initialize array of classes with deleted copy constructor (C++11) which refers to GCC bug 63707.



Related Topics



Leave a reply



Submit