Consider the following example:
class X {
public:
X() = default;
X(const X&) = default;
X(X&&) = delete;
};
X foo() {
X result;
I'm gonna quote C++17 (n4659), since the wording there is the most clear, but previous revisions say the same, just less clearly to me. Here is [class.copy.elision]/3, emphasis mine:
In the following copy-initialization contexts, a move operation might be used instead of a copy operation:
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. [ Note: This two-stage overload resolution must be performed regardless of whether copy elision will occur. It determines the constructor to be called if elision is not performed, and the selected constructor must be accessible even if the call is elided. — end note ]
So this is why in fact both Clang and GCC will try to call the move c'tor first. The difference in behavior is because Clang adheres to the text in bold differently. Overload resolution happened, found a move c'tor, but calling it is ill-formed. So Clang performs it again and finds the copy c'tor.
GCC just stops in its tracks because a deleted function was picked in overload resolution.
I do believe Clang is correct here, in spirit if anything else. Trying to move the returned value and copying as a fallback is the intended behavior of this optimization. I feel GCC should not stop because it found a deleted move c'tor. Neither compiler would if it was a defaulted move c'tor that's defined deleted. The standard is aware of a potential problem in that case ([over.match.funcs]/8):
A defaulted move special function ([class.copy]) that is defined as deleted is excluded from the set of candidate functions in all contexts.
From [class.copy.elision]:
In the following copy-initialization contexts, a move operation might be used instead of a copy operation: 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 [...]
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.
In return result;
we are precisely in the case mentioned in that paragraph - result
is an id-expression naming an object with automatic storage duration declared in the body. So, we first perform overload resolution as if it were an rvalue.
Overload resolution will find two candidates: X(X const&)
and X(X&&)
. The latter is preferred.
Now, what does it mean for overload resolution to fail? From [over.match]/3:
If a best viable function exists and is unique, overload resolution succeeds and produces it as the result. Otherwise overload resolution fails and the invocation is ill-formed.
X(X&&)
is a unique, best viable function, so overload resolution succeeds. This copy elision context has one extra criteria for us, but we satisfy it as well because this candidate has as the type of its first parameter as (possibly cv-qualified) rvalue reference to X
. Both boxes are checked, so we stop there. We do not go on to perform overload resolution again as an lvalue, we have already selected our candidate: the move constructor.
Once we selected it, the program fails because we're trying to call a deleted function. But only at that point, and not any earlier. Candidates are not excluded from overload sets for being deleted, otherwise deleting overloads wouldn't be nearly as useful.
This is clang bug 31025.
result
is not an lvalue after returning from foo()
, but a prvalue, so it is allowed to call the move constructor.
From CppReference:
The following expressions are prvalue expressions:
- a function call or an overloaded operator expression, whose return type is non-reference, such as
str.substr(1, 2)
,str1 + str2
, orit++
It is possible that Clang detected that the return value is unused and threw it away directly, while GCC didn't.