Question

Move elision in explicit object member functions

If one calls explicit object member function of a temporary, must the move of the temporary be elided in the explicit object parameter?

Consider the following example, where struct A has move constructor deleted and f(this A) is invoked for a temporary object A:

struct A {
    A() {}
    A(A&&) = delete;
    void f(this A) {}
};

int main() {
    A{}.f();
}

The program is accepted in GCC, but both Clang and MSVC reject it:

call to deleted constructor of 'A'

error C2280: 'A::A(A &&)': attempting to reference a deleted function

Online demo: https://gcc.godbolt.org/z/rbv14cnz5

Which compiler is correct here?

 4  109  4
1 Jan 1970

Solution

 9

This is CWG2813. As the issue description notes, the wording in C++23 requires a glvalue for the left operand of the dot operator, which implies that if the left operand is a prvalue, it must be converted to a glvalue first (materialized), which prevents copy/move elision. This outcome is undesirable; we would like to have the prvalue initialize the explicit object parameter directly. So, a change was made to the wording and it was approved as a DR. It will take compilers a while to catch up, but the intent is that the example should be accepted.

2024-07-23
Brian Bi

Solution

 2

GCC is correct. Unfortunately, the standard does not contain an explicit statement to this fact. Rather, it is the absence of special wording for explicit object parameters in various places that makes your program correct.

The expression A{}.f() consists of the expression A{}, embedded in a class member access expression A{}.f, which is itself embedded in a function call expression A{}.f(). Starting with the member access, there is no wording in [expr.ref] that requires the receiver operand to be or be converted to a glvalue in this case. Clause 2 imposes this restriction only for non-static data members:

For the first option (dot), if the id-expression [here f] names a static member or an enumerator, ...; if the id-expression names a non-static data member, the first expression shall be a glvalue. ...

The clause (7.3) that handles access to functions just says

If E2 [here f] is an overload set, the expression shall be the (possibly-parenthesized) left-hand operand of a member function call (...), and function overload resolution (...) is used to select the function to which E2 refers. The type of E1.E2 [here E1 = A{}] is the type of E2 and E1.E2 refers to the function referred to by E2.

  • If E2 refers to a static member function, ...
  • Otherwise (when E2 refers to a non-static member function), E1.E2 is a prvalue.

Again, the important part is the lack of any wording restricting A{} to be a glvalue.

Moving on to overload resolution, the relevant rule is [over.call.func]/2:

In qualified function calls, the function is named by an id-expression preceded by an -> or . operator. ... The function declarations found by name lookup (...) constitute the set of candidate functions. The argument list is the expression-list in the call augmented by the addition of the left operand of the . operator in the normalized member function call as the implied object argument (...).

So, in your case, A{}.f() becomes f(A{}) for the purposes of overload resolution, while the overload set in question is simply f(this A). Overload resolution should then succeed. Clang and MSVC's error messages suggest that they get this far successfully.

Explicit object parameters are not handled specially by the rule governing how function parameters are initialized. Implicit object parameters are the special case. [expr.call]/6 reads:

When a function is called, each parameter (...) is initialized (...) with its corresponding argument. If the function is an explicit object member function and there is an implied object argument (...), the list of provided arguments is preceded by the implied object argument for the purposes of this correspondence. ... If the function is an implicit object member function, the object expression of the class member access shall be a glvalue and ....

Again, the sentence which would make your code fail (the last one) does not apply in this case, since f is not an implicit object member function.

Finally, as the function call evaluates, the this A parameter to f needs to be initialized by the prvalue expression A{}. This succeeds, without the involvement of the move constructor, by [dcl.init.general]/16.6

Otherwise, if the destination type is a class type:

  • If the initializer expression is a prvalue and the cv-unqualified version of the source type is the same as the destination type, the initializer expression is used to initialize the destination object.
  • ...

(The expression A{}, after all, does not use the move constructor.)


As an aside, I will point out that your wording of the question is not strictly correct. A{} is not "a temporary"; it is a prvalue. A prvalue is an expression that, when evaluated, initializes a given piece of memory with an object. When a function void g(A); is called by g(A{}), the fact that there is no move should not be thought of as "elision". Thinking in terms of "elision" only makes sense when comparing C++11 to C++03. Since C++11, there is simply no move to elide. Your question is best written "when a function with a by-value explicit object parameter is called on a prvalue of class type, is a temporary materialized and moved from?" The answer I'm giving is "no, the prvalue directly initializes the parameter and there is no temporary and no move."

2024-07-23
HTNW