Why does the following code not result in moving the object instead of copying?
Asked Answered
P

1

6
#include <iostream>
#include <time.h>

class A
{
public:
   A() { std::cout << "a ctor\n"; }
   A(const A&) { std::cout << "a copy ctor\n"; }
   A(A&&) { std::cout << "a move ctor\n"; }
};

A f(int i)
{
   A a1;
   return i != 0 ? a1 : A{};
}

int main()
{
   srand(time(0));
   f(rand());
   return 0;
}

The output is:

a ctor

a copy ctor

I would expect that a1 in f() will be moved not copied. If I change f() just a little bit, it's not a copy anymore but a move:

A f(int i)
{
   A a1;
   if (i != 0)
   {
      return a1;
   }
   return A{};
}

The output is:

a ctor

a move ctor

Could you explain me how does this work? GCC 9.3.0

(edit: added random number to prevent RVO)

Pedometer answered 27/6, 2020 at 14:4 Comment(5)
What compiler etc? GCC 9.2 gives me double ctor, that's all.Thebes
Is this a typo? I get a ctor a copy ctor with f(4) but a ctor a ctor with f(5). The same in the second example. f(5) gives a ctor a ctor and f(4) gives a ctor a move ctorTangleberry
I had an older compiler which did not perform RVO on the original code (that's why you saw "a ctor a ctor" output), so I've updated it with random number generation to prevent RVO.Copyread
all of your constructors has side effects with the cout stuff. cout is of course not movable and not copyable.Enlace
What kind of side effects do you mean exactly? The class does not have cout as a member, it only uses it. I'm not seeing how cout is interfering the movability of the class.Copyread
C
2

The difference you're seeing is due to the use of the ternary/conditional operator. The ternary operator determines a common type and value category for its second and third operands, and that is determined at compile time. See here.

In your case:

return i != 0 ? a1 : A{};

the common type is A and the common value category is prvalue since A{} is an prvalue. However, a1 is an lvalue and a prvalue temporary copy of it will have to be made in the lvalue-to-rvalue conversion. This explains why you see the copy constructor invoked when the condition is true: a copy of a1 is made to convert it to an prvalue. The prvalue is copy elided by your compiler.

In the second example, where you have an if statement, these rules don't apply as in the case of the ternary operator. So no copy constructor invoked here.


To address your comment about a conditional statement with lvalue for the second and third operands, according to the rules of copy elision it is allowed if:

In a return statement, when the operand is the name of a non-volatile object with automatic storage duration, which isn't a function parameter or a catch clause parameter, and which is of the same class type (ignoring cv-qualification) as the function return type. This variant of copy elision is known as NRVO, "named return value optimization".

A conditional statement like

return i != 0 ? a1 : a1;

where the second and third operands are lvalues, does not fulfill this criteria. The expression is a conditional, not the name of an automatic object. Hence no copy elision and the copy constructor is invoked.

Chalone answered 27/6, 2020 at 16:53 Comment(2)
Thanks for your answer! Actually what you wrote made sense to me, but I've tried the following: return i != 0 ? a1 : a1; In this case I would think the common value category is lvalue since a1 is an lvalue, but again I see copy constructor called. However, if I explicitly make it into an rvalue reference then move constructor is invoked: return i != ? std::move(a1) : a1; I'm still confused.Copyread
@PeresztegiPéter See my update. Hope this answers your question.Chalone

© 2022 - 2024 — McMap. All rights reserved.