6

The following code:

#include <iostream>

struct A {
  A() { std::cout << "()" << std::endl; }  
  A(A&&) { std::cout << "(A&&)" << std::endl; }  
  A(const A&) { std::cout << "(const A&)" << std::endl; }  
};

A fun (A&& a){
  return a;
}

int main(){
  A a;
  fun(std::move(a));
  return 0;
}

in clang 16.0.3 (arm64-apple-darwin22.4.0 with c++17) produces

()
(A&&)

while most compilers give

()
(const A&)

Which one is correct?

sakurai
  • 78
  • 7
Kozmar
  • 77
  • 1
  • 2
    MSVC agrees with clang, here. In fact, most of the major compilers on Compiler Explorer give the first output (correctly, IMHO). Only GCC and ICC give the second, and note that ICX gives the first. – Adrian Mole May 08 '23 at 10:52
  • 2
    @Jason Why a clang bug? The c'tor call uses an rvalue reference, so the rvalue c'tor should be called, is not? – Adrian Mole May 08 '23 at 10:58
  • 1
    When I test this on gcc with `-std=c++17` or below, I get `(const A&)`, but with `-std=c++20` or above, I get `(A&&)`. On clang, I always get `(A&&)`. – Andreas Wenzel May 08 '23 at 10:59
  • @Ted In [here](https://onlinegdb.com/I2-fxKOD2) you get different results depending on the C+17 or C+20 setting. I expected a named reference `a` inside the function to be treated as lvalue reference (which would make clang incorrect). – Kozmar May 08 '23 at 11:04
  • @AndreasWenzel Aha, c++17, I missed that detail. Kozmar: Yes, I didn't notice the C++17 tag. – Ted Lyngmo May 08 '23 at 11:04
  • @Kozmar But using `std::move()` **explicitly casts** the argument to an rvalue reference. – Adrian Mole May 08 '23 at 11:07
  • 2
    @AdrianMole the constructor is for `return a;` inside the function. No consturctor is called to pass the argument in `fun(std::move(a));` – 463035818_is_not_an_ai May 08 '23 at 11:08
  • @463035818_is_not_a_number OK - Seems I am getting confuddled. – Adrian Mole May 08 '23 at 11:10
  • @AdrianMole I mean `A(&&)` is better match than `A(const A&)` here so it should be used. I deleted my last incorrect comment. So `A(&&)` seems to be the correct output. – Jason May 08 '23 at 11:11
  • @AdrianMole Ah i remember due to implicit move rule, the move ctor should be used here. – Jason May 08 '23 at 11:21
  • 3
    Related? Duplicate? [Why does C++11 have implicit moves for value parameters, but not for rvalue parameters?](https://stackoverflow.com/q/9779079/10871073) – Adrian Mole May 08 '23 at 11:24
  • @AdrianMole Seems like it. – Jason May 08 '23 at 11:26
  • @Jason I do like the top answer to that question, especially the bit about *when it is clearly safe to do so* (as in when you are returning it). – Adrian Mole May 08 '23 at 11:28
  • @AdrianMole I found a better [dupe](https://stackoverflow.com/questions/71898998/return-a-named-object-of-a-class-from-a-function-by-value-and-implicit-move-r) with a trivial difference that here the first step in overload resolution succeeds while in the dupe the first step failed so we went onto second step. – Jason May 08 '23 at 11:33
  • @Jason Maybe just delete your answer, then? – Adrian Mole May 08 '23 at 11:46
  • @AdrianMole thanks for all the help! If I am reading it correctly, your link indicates that I should expect (const A&) (just as the MSVC 2010 does in that question). This would agree with that Jason is saying: the implicit move rule for rvalue references is from C++20. I.e. all C++20 compilers are obliged to do (A&&), while earlier should (const A&). Am I right? – Kozmar May 08 '23 at 17:11

1 Answers1

1

The move constructor A::A(A&&) should be used here due to the implicit move rule for local variable and parameters when returning by value:

If expression is a (possibly parenthesized) id-expression that names a variable whose type is either

  • a non-volatile object type or

  • a non-volatile rvalue reference to object type (since C++20)

and that variable is declared

  • in the body or

  • as a parameter of

    the innermost enclosing function or lambda expression,

then overload resolution to select the constructor to use for initialization of the returned value or, for co_return, to select the overload of promise.return_value() (since C++20) is performed twice:

  • first as if expression were an rvalue expression (thus it may select the move constructor), and
  • if the first overload resolution failed or
  • it succeeded, but did not select the move constructor (formally, the first parameter of the selected constructor was not an rvalue reference to the (possibly cv-qualified) type of expression) (until C++20)
  • then overload resolution is performed as usual, with expression considered as an lvalue (so it may select the copy constructor).

(emphasis mine)

In your example, you have the statement return a; and a is a parameter of the innermost enclosing function, thus overload resolution can be performed twice. In the first step a is treated as if it were an rvalue expression and so the move constructor can be used here.

Note there is no need to perform the second step as the first step succeeded.

Jason
  • 36,170
  • 5
  • 26
  • 60