Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What constructor should be called in return statement with curly braces?

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?

like image 964
αλεχολυτ Avatar asked Mar 19 '21 12:03

αλεχολυτ


1 Answers

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:

  • Clang 11: accepts both (A1) and (A2) for C++14 through C++20
  • Clang 13: rejects both (A1) and (A2) for C++14 through C++20
  • GCC 10: accepts both (A1) and (A2) for C++14 through C++20
  • GCC 11: accepts both (A1) and (A2) for C++14 through C++17; rejects both for C++20
  • MSVC: rejects both (A1) and (A2) for C++14 through C++20

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:

  • Clang 11: accepts both (B1) and (B2) for C++14 through C++20
  • Clang 13: accepts both (B1) and (B2) for C++14 through C++20
  • GCC 10: accepts both (B1) and (B2) for C++14 through C++20
  • GCC 11: accepts both (B1) and (B2) for C++14 through C++20
  • MSVC: rejects both (B1) and (B2) for C++14 through C++20

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.

like image 90
dfrib Avatar answered Nov 20 '22 21:11

dfrib