Why Doesn't C++ Move Construct Rvalue References by Default

Why doesn't C++ move construct rvalue references by default?

with your design:

void doWork(Widget && param)
{
Widget store1 = param; // automatically move param
Widget store2 = param; // boom

Widget store_last = param; // boom
}

with current design:

void doWork(Widget && param)
{
Widget store1 = param; // ok, copy
Widget store2 = param; // ok, copy

Widget store_last = std::move(param); // ok, param is moved at its last use
}

So the moral here is that even if you have an rvalue reference you have a name for it which means you can use it multiple times. As such you can't automatically move it because you could need it for a later use.


Now let's say you want to re-design the language so that the last use is automatically treated as an rvalue.

This can be easily done in the above example:

void doWork(Widget && param)
{
Widget store1 = param; // `param` treated as lvalue here, copy
Widget store2 = param; // `param` treated as lvalue here, copy

Widget store_last = param; // `param` treated as rvalue here, move
}

Let's ignore the inconsistency of how param is treated (which in itself is a problem).

Now think what use of param is the last use:

void doWork(Widget && param)
{
Widget store2 = param; // this can be last use or not

while (some_condition())
{
Widget store1 = param; // this can be both last use and not
}
}

The language simply cannot be designed this way.

C++ Why does returning rvalue reference change caller's behavior when function signature does not return rvalue reference?

What's happening is you are seeing elision there. You are move-constructing on return std::move(x) with a simple type of Bar; then the compiler is eliding the copy.

You can see the non-optimized assembly of GetBarRValue here. The call to the move constructor is actually happening in the GetBarRValue function, not upon returning. Back in main, it's just doing a simple lea, it's not at all calling any constructor.

Why isn't the move constructor being called?

Why doesn't Class obj2(obj1.fun()); call the move constructor

Because of Copy Elison. The compiler sees that fun() returns a temporary object, and that temporary will only be used to initialize obj2, so the compiler optimizes the creation of obj2 by eliminating the temporary object altogether and allowing obj2 to be created directly inside of fun() itself, thus there is no need for a copy/move operation when fun() exits.

Class obj2(std::move(obj1.fun())); does call the move constructor

Because you are forcing it with the explicit std::move type-cast, so the compiler can't optimize the creation of obj2 via Copy Elison, so it has to allow fun() to return a temporary object, which you are then moving into the obj2 constructor.

obj3 = obj2.fun() calls the (move) assignment operator

Because obj3 already exists before the assignment.

without needing to write std::move(obj2.fun())

Because fun() returns a temporary object, which is an rvalue, so there is no need to explicitly type-cast it to an rvalue when calling a move assignment operator.

When should we declare rvalue refs if they actually need std::move?

My question is, what's the utility of defining a variable as A&&? I could perfectly define it as A& and move it exactly the same.

If a named variables is passed into a function and modified like that without warning it's very displeasing, and can lead to unpleasant debugging sessions.

When you take by rvalue reference the type system enforces useful guarantees. Either the reference is bound to an rvalue, and so modifying it isn't going to violate anyone's assumptions (it's gone soon anyway). Or you were given explicit permission to move out of it. The calling context can't accidentally give you permission to modify an lvalue, it must do so with an explicit cast (std::move).

Explicit is better than implicit. And having rvalue references interact with the type system the way they do helps make sure moving does not leave objects in an unexpected state. A glance at the calling context reveals that the variable passed into the function may be left with an unknown state, because it's explicitly surrounded with a call to std::move.

That's the benefit of rvalue references.

Why is `T(const T&&)` called a move constructor?

Here are some of the differences between move constructors and other constructors:

  • Move constructors can be defaulted
  • Move constructors don't prevent a type from being a "literal type"
  • Non-trivial move constructors prevent a type from being a "trivially copyable type"
  • Move constructors prevent the implicit move constructor from being generated
  • Move constructors may be automatically called by standard library functions

As far as I can tell, for all of those, not calling X(const X &&) a move constructor gives undesirable results.

You give an alternative: it might be called a copy constructor instead. That too seems to have undesirable results: it would suppress the implicit copy constructor.

Whether a move constructor actually moves doesn't matter. A POD type may have a move constructor too. It'll just be making a copy, but it's still called a move constructor.

Why rvalue references are connected with move semantics?

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.

Confusion with move constructors: unable to call move constructor

The reason why you don't see any output from line 3 is that it declares a function, not a variable. This is due to an ambiguity called Most Vexing Parse.

Compare c s3(c()) with int foo(int ()), which, thanks to implicit type adjustments, is the same as int foo(int (*f)()).

To get around this, use brace initialisation (which was actually introduced in C++11 partly for this very reason):

c s3(c{});

// or

c s3{c()};

// or

c s3{c{}};

Lvalue reference constructor is called instead of rvalue reference constructor

Why F(F&) constructor is called instead of F(F&&) constructor in constructor of class G?

Because f is an lvalue. Even though it is bound to an rvalue, and its type is rvalue reference to F, it is also a named variable. That makes it an lvalue. Don't forget that the value category of an object is not determined by its type, and vice versa.

When you pass an lvalue to a function, only lvalue references can be bound to it. You should change your code as follows if you want to catch rvalues only:

class G {
F f_;
public:
G(F&& f) : f_(std::move(f)) {
std::cout << "G()" << std::endl;
}
};

Alternatively, you could use std::forward<>(), which is equivalent in this case, but makes your intent of forwarding f even clearer:

class G {
F f_;
public:
G(F&& f) : f_(std::forward<F>(f)) {
std::cout << "G()" << std::endl;
}
};

Now this last definition is easy to extend so that both lvalues and rvalues of type F can be bound to the parameter f:

class G {
F f_;
public:
template<typename F>
G(F&& f) : f_(std::forward<F>(f)) {
std::cout << "G()" << std::endl;
}
};

This allows, for instance, to construct an instance of G this way:

F f;
G g(f); // Would not be possible with a constructor accepting only rvalues

This last version has a caveat though: your constructor will basically work as a copy-constructor as well, so you might want to explicitly define all the possible copy constructors to avoid awkward situations:

class G {
F f_;
public:
template<typename F>
G(F&& f) : f_(std::forward<F>(f)) {
std::cout << "G()" << std::endl;
}
G(G const&) = default;
G(G&); // Must be defaulted out-of-class because of the reference to non-const
};

G::G(G&) = default;

Since non-template functions are preferred over instantiations of function templates, the copy constructor will be selected when constructing a G object from another G object. The same applies, of course, to the move constructor. This is left as an exercise.

What happens when std::move is called on a rvalue reference?

string constructed = s;

This does not cause a move because s is not an rvalue. It is an rvalue reference, but not an rvalue. If it has a name, it is not an rvalue. Even if the variable's type is rvalue reference, the expression consisting of its name is an lvalue expression.

string constructed = std::move(s);

This causes a move because std::move(s) is an rvalue: it's a temporary and its type is not lvalue reference.

There are no other moves in the program (std::move is not a move, it's a cast).



Related Topics



Leave a reply



Submit