Consider the following code:
struct NonMovable {
NonMovable() = default;
NonMovable(const NonMovable&) = default;
NonMovable(NonMovable&&) = delete;
};
NonMovable f() {
NonMovable nonMovable;
return {nonMovable};
//return NonMovable(nonMovable);
}
int main() {}
GCC and Clang compile that code without errors, i.e. copy constructor was called when curly braces are used. But msvc reject it https://godbolt.org/z/49onKj with the error:
error C2280: 'NonMovable::NonMovable(NonMovable &&)': attempting to reference a deleted function
When I specify explicit call to copy constructor (since nonMovable
is not an rvalue) then mvsc accepts the code.
Who's correct? What type of constructor should be called in return {var};
statement there?
There is quite some implementation confusion regarding return value copy elision and temporary materialization. To begin with, let's have a look at the OP's example slightly modified to not use the braced-init-list in the return statement:
// Program (A1)
struct NonMovable {
NonMovable() = default;
NonMovable(const NonMovable&) = default;
NonMovable(NonMovable&&) = delete; // #1
};
NonMovable f() {
NonMovable nonMovable;
return nonMovable; // #2
// GCC, Clang: OK
// MSVC: Error (use of deleted function)
}
int main() {}
#1
formally provides an explicitly-deleted definition for the move constructor, meaning the move constructor will participate in overload resolution, and its lack of a non-deleted definition will have no effect on the result of overload resolution.
Now, as per [class.copy.elision]/3, overload resolution for the copy-init context of #2 is (possibly) two-phase, starting to look for an ctor overload as if the object were designated by an rvalue:
In the following copy-initialization contexts, a move operation might be used instead of a copy operation:
(3.1) If the expression in a 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 [...]
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 an rvalue reference to the object's type (possibly cv-qualified), overload resolution is performed again, considering the object as an lvalue.
The key here being that the second phase is only performed if the first phase overload resolution fails (or was not performed). In the example above, the first phase overload resolution will find the deleted move ctor, and the second phase will not be performed. Thus, solely based on [class.copy.elision]/3, one would argue that program (A) is ill-formed.
On the other hand, all non-deleted constructors in NonMovable
are trivial, meaning we may turn to [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. The temporary object is constructed from the function argument or return value, respectively, and the function's parameter or return object is initialized as if by using the non-deleted trivial constructor to copy the temporary (even if that constructor is inaccessible or would not be selected by overload resolution to perform a copy or move of the object).
Which would arguably overrule the fact that [class.copy.elision]/3 would otherwise reject the program.
Before going into what the compilers actually do here, consider a slightly modified version of program (A1) above:
// Program (A2)
struct NonMovable {
NonMovable() {};
NonMovable(const NonMovable&) {};
NonMovable(NonMovable&&) = delete; // #1
};
// ... as above
where the default and copy ctors have now been made non-trivial as they are user-provided.
Then:
If we tweak program (A1) and (A2) but wrapping the return object in braces,
// ... as above
return {nonMovable};
denoting the corresponding programs as (B1) and (B2), then:
Who's correct? What type of constructor should be called in
return {var};
statement there?
To wrap up and answer the OP's original question, MSVC is wrong to reject (B1) and (B2), as [class.copy.elision]/3, particularly /3.1, does not apply when the expression in a return statement is a braced-init-list, even if it wraps a named object with automatic storage duration. This case is simply braced-copy-init, and the copy constructor shall be the best viable function resulting from overload resolution.
The implementation variance for (A1) and (A2) likely relates to P1825R0: Merged wording for P0527R1 and P1155R3 (more implicit moves), which would explain why both GCC 11 and Clang 13 now rejects both (A1) and (A2) for C++20, and we may moreover note that GCC and Clang particularly marks P1825R0 as implemented in release 11 and 13, respectively.
I do not understand, however, why Clang 13 also seems to have backported this, particularly rejecting (A1) and (A2) also for C++14 and C++17. We may note that Clang (even in earlier version) also rejects the example of [diff.cpp17.class]/3 (added to the C++20 standard as part of P1825R0, highlighting a compatibility change from C++17) in C++14 and C++17 which is arguably a Clang bug. I'm speculating that the backporting rejection of (A1) and (A2) is unintentional.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With