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)

  • 2
    What compiler etc? [GCC 9.2](https://wandbox.org/permlink/Of983X2o8jy8RsNO) gives me double ctor, that's all. – Hatted Rooster Jun 27 '20 at 14:08
  • 1
    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 ctor` – Thomas Sablik Jun 27 '20 at 14:35
  • 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. – Peresztegi Péter Jun 27 '20 at 15:21
  • all of your constructors has side effects with the cout stuff. cout is of course not movable and not copyable. – Captain Giraffe Jun 28 '20 at 01:44
  • 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. – Peresztegi Péter Jun 28 '20 at 15:27

1 Answers1

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.

jignatius
  • 6,304
  • 2
  • 15
  • 30
  • 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. – Peresztegi Péter Jun 27 '20 at 18:33
  • 1
    @PeresztegiPéter See my update. Hope this answers your question. – jignatius Jun 27 '20 at 21:17