Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Copy ctor called instead of move ctor

Why is the copy constructor called when returning from bar instead of the move constructor?

#include <iostream>

using namespace std;

class Alpha {
public:
  Alpha() { cout << "ctor" << endl; }
  Alpha(Alpha &) { cout << "copy ctor" << endl; }
  Alpha(Alpha &&) { cout << "move ctor" << endl; }
  Alpha &operator=(Alpha &) { cout << "copy asgn op" << endl; }
  Alpha &operator=(Alpha &&) { cout << "move asgn op" << endl; }
};

Alpha foo(Alpha a) {
  return a; // Move ctor is called (expected).
}

Alpha bar(Alpha &&a) {
  return a; // Copy ctor is called (unexpected).
}

int main() {
  Alpha a, b;
  a = foo(a);
  a = foo(Alpha());
  a = bar(Alpha());
  b = a;
  return 0;
}

If bar does return move(a) then the behavior is as expected. I do not understand why a call to std::move is necessary given that foo calls the move constructor when returning.

like image 680
Jacob Pollack Avatar asked Aug 22 '17 02:08

Jacob Pollack


3 Answers

There are 2 things to understand in this situation:

  1. a in bar(Alpha &&a) is a named rvalue reference; therefore, treated as an lvalue.
  2. a is still a reference.

Part 1

Since a in bar(Alpha &&a) is a named rvalue reference, its treated as an lvalue. The motivation behind treating named rvalue references as lvalues is to provide safety. Consider the following,

Alpha bar(Alpha &&a) {
  baz(a);
  qux(a);
  return a;
}

If baz(a) considered a as an rvalue then it is free to call the move constructor and qux(a) may be invalid. The standard avoids this problem by treating named rvalue references as lvalues.

Part 2

Since a is still a reference (and may refer to an object outside of the scope of bar), bar calls the copy constructor when returning. The motivation behind this behavior is to provide safety.

References

  1. SO Q&A - return by rvalue reference
  2. Comment by Kerrek SB
like image 56
Jacob Pollack Avatar answered Oct 20 '22 11:10

Jacob Pollack


yeah, very confusing. I would like to cite another SO post here implicite move. where I find the following comments a bit convincing,

And therefore, the standards committee decided that you had to be explicit about the move for any named variable, regardless of its reference type

Actually "&&" is already indicating let-go and at the time when you do "return", it is safe enough to do move.

probably it is just the choice from standard committee.

item 25 of "effective modern c++" by scott meyers, also summarized this, without giving much explanations.

Alpha foo() {
  Alpha a
  return a; // RVO by decent compiler
}
Alpha foo(Alpha a) {
  return a; // implicit std::move by compiler
}

Alpha bar(Alpha &&a) {
  return a; // Copy ctor due to lvalue
}

Alpha bar(Alpha &&a) {
  return std:move(a); // has to be explicit by developer
}
like image 1
pepero Avatar answered Oct 20 '22 11:10

pepero


This is a very very common mistake to make as people first learn about rvalue references. The basic problem is a confusion between type and value category.

int is a type. int& is a different type. int&& is yet another type. These are all different types.

lvalues and rvalues are things called value categories. Please check out the fantastic chart here: What are rvalues, lvalues, xvalues, glvalues, and prvalues?. You can see that in addition to lvalues and rvalues, we also have prvalues and glvalues and xvalues, and they form a various venn diagram sort of relation.

C++ has rules that say that variables of various types can bind to expressions. An expressions reference type however, is discarded (people often say that expressions do not have reference type). Instead, the expression have a value category, which determines which variables can bind to it.

Put another way: rvalue references and lvalue references are only directly relevant on the left hand of the assignment, the variable being created/bound. On the right side, we are talking about expressions and not variables, and rvalue/lvalue reference-ness is only relevant in the context of determining value category.

A very simple example to start with is simple looking at things of purely type int. A variable of type int as an expression, is an lvalue. However, an expression consisting of evaluating a function that returns an int, is an rvalue. This makes intuitive sense to most people; the key thing though is to separate out the type of an expression (even before references are discarded) and its value category.

What this is leading to, is that even though variables of type int&& can only bind to rvalues, does not mean that all expressions with type int&&, are rvalues. In fact, as the rules at http://en.cppreference.com/w/cpp/language/value_category say, any expression consisting of naming a variable, is always an lvalue, no matter the type.

That's why you need std::move in order to pass along rvalue references into subsequent functions that take by rvalue reference. It's because rvalue references do not bind to other rvalue references. They bind to rvalues. If you want to get the move constructor, you need to give it an rvalue to bind to, and a named rvalue reference is not an rvalue.

std::move is a function that returns an rvalue reference. And what's the value category of such an expression? An rvalue? Nope. It's an xvalue. Which is basically an rvalue, with some additional properties.

like image 1
Nir Friedman Avatar answered Oct 20 '22 09:10

Nir Friedman