scdmb
scdmb

Reputation: 15641

Why rvalue references are connected with move semantics?

As I read some articles, rvalue references and move semantics are usually described together. However as I understand, rvalue references are just references to rvalues and have nothing to do on their own with move semantics. And move semantics could be implemented probably without even using rvalue references. So the question is, why move constructor/operator= use rvalue references? Was it just to make it easier to write the code?

Upvotes: 3

Views: 493

Answers (2)

Mankarse
Mankarse

Reputation: 40633

The connection is that it is safe to move from an rvalue (because (in the absence of casts) rvalues refer to objects that are at the end of their lifespans), so a constructor that takes an rvalue reference can be safely implemented by pilfering/moving from the referenced object.

From a C++-language point of view, this is the end of the connection, but the standard library further expands on this connection by consistently making construction from lvalues copy and construction from rvalues move, and by providing helper functions (such as std::move) which make it straightforward to chose whether to move or copy a particular object (by changing around the value category of the object in the expression that causes the copy/move).

Move semantics can be implemented without rvalue-references, but it would be a lot less neat. A number of problems would need to be solved:

  1. How to capture an rvalue by non-const reference?

  2. How to distinguish between a constructor that copies and a constructor that moves?

  3. How to ensure that moves are used wherever they would be a safe optimization?

  4. How to write generic code that works with both movable and copyable objects?

Upvotes: 1

Nicol Bolas
Nicol Bolas

Reputation: 474326

Consider the problem. There are two basic move operations we want to support: move "construction" and move "assignment". I use quotations there because we don't necessarily have to implement them with constructors or move assignment operators; we could use something else.

Move "construction" means creating a new object by transferring the contents from an existing object, such that deleting the old object doesn't deallocate resources now used in the new one. Move "assignment" means taking a pre-existing object and transferring the contents from an existing object, such that deleting the old object doesn't deallocate resources now used in the new one.

OK, so these are the operations we want to do. Well, how to do it?

Take move "construction". While we don't have to implement this with a constructor call, we really want to. We don't want to force people to do two-stage move construction, even if it's behind some magical function call. So we want to be able to implement movement as a constructor. OK, fine.

Here's problem 1: constructors have no names. Therefore, you can only differentiate them based on argument types and overloading resolution. And we know that the move constructor for an object of type T must take an object of type T as a parameter. And since it only needs one argument, it therefore looks exactly like a copy constructor.

OK, so now we need some way to satisfy overloading. We could introduce some standard library type, a std::move_ref. It would be like std::reference_wrapper, but it would be a distinct type. Therefore, you could say that a move constructor is a constructor that takes a std::move_ref<T>. Alright, fine: problem solved.

Only not; we now have new problems. Consider this code:

std::string MakeAString() { return std::string("foo"); }

std::string data = MakeAString();

Ignoring elision, C++11's expression value category rules state that a type which is returned from a function by value is an prvalue. And therefore, it will automatically be used by move constructors/assignment operators wherever possible. No need for std::move or the like.

To do it your way would require this:

std::string MakeAString() { return std::move(std::string("foo")); }

std::string data = std::move(MakeAString());

Both of those std::move calls would be needed to avoid copying. You have to move out of the temporary and into the return value, and then move out of the return value and into data (again, ignoring elision).

If you think that this is merely a minor annoyance, consider what else rvalue references buy us: perfect forwarding. Without the special reference-collapsing rules, you could not write a proper forwarding function that forwards copy and move semantics perfectly. std::move_ref would be a real C++ type; you couldn't just slap arbitrary rules like reference collapsing onto it like you can with rvalue references.

At the end of the day, you need some kind of language construct in place, not merely a library type. By making it a new kind of reference, you get to be able to define new rules for what can bind to that reference (and what cannot). And you get to define special reference-collapsing rules that make perfect forwarding possible.

Upvotes: 8

Related Questions