Alessandro Power
Alessandro Power

Reputation: 2472

Constructor called on return statement

Consider the following example:

class X {
public:
    X() = default;
    X(const X&) = default;
    X(X&&) = delete;
};

X foo() {
    X result;
    return result;
}

int main() {
    foo();
}

Clang and GCC disagree on whether this program is valid. GCC tries to call the move constructor when initializing the temporary during the call to foo(), which has been deleted leading to a compilation error. Clang handles this just fine, even with -fno-elide-constructors.

Can anyone explain why GCC is allowed to call the move constructor in this case? Isn't result an lvalue?

Upvotes: 14

Views: 1337

Answers (3)

Barry
Barry

Reputation: 304182

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.

Upvotes: 8

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.

Upvotes: 12

iBug
iBug

Reputation: 37327

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, or it++

It is possible that Clang detected that the return value is unused and threw it away directly, while GCC didn't.

Upvotes: 0

Related Questions