4

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?

3
  • I think GCC is correct here. There is nothing saying that the implied object argument should behave different than any other argument. The only exception where a prvalue-to-xvalue conversion is forced is for implicit object functions (eel.is/c++draft/expr.call#6.sentence-4). Commented Jul 23, 2024 at 19:42
  • 1
    In fact, I would be surprised if GCC was not supposed to be correct here: One of the intentions of by-value explicit object parameters is to make chaining of method calls efficient. There shouldn't be redundant moves from one return value into the parameter of the next call in the chain. Commented Jul 23, 2024 at 19:46
  • My gut instinct is that any expression of the form prvalue.stuff must manifest a temporary out of prvalue before doing whatever is in stuff. But a cursory examination of the standard doesn't really spell this out anywhere. Commented Jul 23, 2024 at 19:54

2 Answers 2

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.

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks! Clang implemented this feature today: github.com/llvm/llvm-project/issues/100314
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."

1 Comment

It should be noted that the statement in [expr.ref]/2 has changed fairly recently. N4590 (from May 2023) just says, "For the first option (dot) the first expression shall be a glvalue." And by declaring that it "shall be a glvalue", that provokes temporary materialization. I don't know when C++23 was published, but this could be some kind of defect fix or deliberate change post-C++23 that the other compilers haven't adopted.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.