Normally, elision is an optimization. While virtually every compiler support copy elision in the simplest of cases, having elision still places a particular burden on users. Namely, the type who's copy/move is being elided must still have the copy/move operation that was elided.
For example:
std::mutex a_mutex;
std::lock_guard<std::mutex> get_lock()
{
return std::lock_guard<std::mutex>(a_mutex);
}
This might be useful in cases where a_mutex
is a mutex that is privately held by some system, yet an external user might want to have a scoped lock to it.
This is also not legal, because std::lock_guard
cannot be copied or moved. Even though virtually every C++ compiler will elide the copy/move, the standard still requires the type to have that operation available.
Until C++17.
C++17 mandates elision by effectively redefining the very meaning of certain expressions so that no copy/moving takes place. Consider the above code.
Under pre-C++17 wording, that code says to create a temporary and then use the temporary to copy/move into the return value, but the temporary copy can be elided. Under C++17 wording, that does not create a temporary at all.
In C++17, any prvalue expression, when used to initialize an object of the same type as the expression, does not generate a temporary. The expression directly initializes that object. If you return a prvalue of the same type as the return value, then the type need not have a copy/move constructor. And therefore, under C++17 rules, the above code can work.
The C++17 wording works in cases where the prvalue's type matches the type being initialized. So given get_lock
above, this will also not require a copy/move:
std::lock_guard the_lock = get_lock();
Since the result of get_lock
is a prvalue expression being used to initialize an object of the same type, no copying or moving will happen. That expression never creates a temporary; it is used to directly initialize the_lock
. There is no elision because there is no copy/move to be elided elide.
The term "guaranteed copy elision" is therefore something of a misnomer, but that is the name of the feature as it is proposed for C++17 standardization. It does not guarantee elision at all; it eliminates the copy/move altogether, redefining C++ so that there never was a copy/move to be elided.
This feature only works in cases involving a prvalue expression. As such, this uses the usual elision rules:
std::mutex a_mutex;
std::lock_guard<std::mutex> get_lock()
{
std::lock_guard<std::mutex> my_lock(a_mutex);
//Do stuff
return my_lock;
}
While this is a valid case for copy elision, C++17 rules do not eliminate the copy/move in this case. As such, the type must still have a copy/move constructor to use to initialize the return value. And since lock_guard
does not, this is still a compile error.
Implementations are allowed to refuse to elide copies when passing or returning an object of trivially-copyable type. This is to allow moving such objects around in registers, which some ABIs might mandate in their calling conventions.
struct trivially_copyable {
int a;
};
void foo (trivially_copyable a) {}
foo(trivially_copyable{}); //copy elision not mandated