Ari
Ari

Reputation: 7556

Calling std::move without move constructor or move assignment

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.

Upvotes: 1

Views: 1623

Answers (1)

templatetypedef
templatetypedef

Reputation: 372704

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."

Upvotes: 7

Related Questions