1

We know that std::move does not actually move anything. It just cast an lvalue reference (&) to rvalue reference (&&).

Then how in the following example, the copy constructor is called? If there is not move constructor, how does constructing an object that is using std::move() falls back on copy constructor? Exactly how does this binding for variable b happen?

struct Test {
  // Default constructor
  Test() {
    std::cout << "Constructor is called." << std::endl;
    mValue = 0;
  }
  
  // Copy constructor
  Test(const Test& rhs) {
    std::cout << "Copy Constructor is called." << std::endl;
    mName = rhs.mName;
    mValue = rhs.mValue;
  }
    
  std::string mName;
  int mValue;
};

int main() {
  Test a;
  Test b = std::move(a);
  return 0;
}

Output:

Constructor is called.
Copy Constructor is called.
Ari
  • 7,251
  • 11
  • 40
  • 70
  • 1
    Because rvalue reference can be coerced to a const lvalue reference if there is not a better candidate, and that's what happens. – bipll Sep 11 '20 at 19:02
  • 2
    A const lvalue reference can bind to an rvalue. This dates back to pre-C++11 rules. – templatetypedef Sep 11 '20 at 19:10

1 Answers1

5

Let's reason by analogy. Think about this code:

void doSomething(const int& x) {
    std::cout << "You like " << x << "? That's my favorite number!" << std::endl;
}

int main() {
    doSomething(137); // <-- Here
}

Now, focus on the call in main. This code compiles and runs just fine, but there's something a bit weird about it. Notice that doSomething takes in a const int&. That means it takes in a reference to an int, and references (normally) only bind to lvalues. But the argument here, 137, is an rvalue. What gives?

The reason this works is that the C++ language specifically allows for const lvalue references to bind to rvalues, even though regular lvalue references can't. For example:

const int& totallyLegal = 137; // Yep, that's fine!
int& whoaNotCoolMan     = 42;  // Compile error!

There are a couple of reasons why you can do this. If you have a const lvalue reference, you've promised that you're allowed to look at the referenced object, but you can't modify it. Therefore, it's safe to bind the lvalue reference to an rvalue, since you wouldn't then have a way to take a "pure value" and assign it something. And historically, pre-C++11, when rvalue references didn't exist, this made it possible to write functions that would say "please pass this argument to me in a way that doesn't involve making copies" by using a const lvalue reference.

Now that we have lvalue references, this rule introduces some points of confusion that weren't previously there. In particular, a const T& can bind to the result of any expression of type T, even if it's a T& or a T&&. That's why the copy constructor is selected in your case.

There is one further nuance here, though. The same way that the C++ compiler will automatically define a default constructor, copy constructor, and assignment operator for a class provided that you don't do so yourself, the C++ compiler can automatically define a move constructor as well. However, there's a rule saying that if a type has a user-defined copy constructor, then the compiler won't generate a move constructor for you. So the full answer to your question is "the existence of your copy constructor means that no move constructor is defined, and since the copy constructor takes in a const lvalue reference it'll bind to rvalues as well as lvalues."

Dharman
  • 30,962
  • 25
  • 85
  • 135
templatetypedef
  • 362,284
  • 104
  • 897
  • 1,065
  • If there was a move constructor, the rvalue would bind to it. This suggests that there is a rule that prioritizes the binding of an rvalue: first try binding it to the move constructor (rvalue reference), if not successful, bind it to the copy constructor (const lvalue reference). What is the name or a referent to this rule? Also, based on what you said, if there was no copy constructor or move constructor, bind it to the automatically defined move constructor. – Ari Sep 11 '20 at 23:32
  • I don’t think there’s a specific name for the rule. It’s a consequence of how overload selection works. There’s a series of rules for determining how “good” an overload selection is, and it’s better to bind an rvalue reference to an rvalue than to bind a const rvalue reference to an rvalue. – templatetypedef Sep 12 '20 at 00:05