5
votes

I was reading Effective Modern C++ Item 25, on page 172, it has an example to demonstrate that, if you want to move return an rvalue reference parameter, you need to wrap it with std::move(param). As parameter by itself is always an lvalue, if no std::move(), it will be copy returned.

I don't understand. If std::move(param) merely cast the parameter it takes in into an rvalue reference, then what's the difference when param is already an rvalue reference?

Like in the code below:

#include <string>
#include <iostream>
#include <utility>

template<typename T>
class TD;

class Widget {
public:
    explicit Widget(const std::string& name) : name(name) {
        std::cout << "Widget created with name: " << name << ".\n";
    }

    Widget(const Widget& w) : name(w.name) {
        std::cout << "Widget " << name << " just got copied.\n";
    }

    Widget(Widget&& w) : name(std::move(w.name)) {
        std::cout << "Widget " << name << " just got moved.\n";
    }

private:
    std::string name;
};

Widget passThroughMove(Widget&& w) {
    // TD<decltype(w)> wType;
    // TD<decltype(std::move(w))> mwType;
    return std::move(w);
}

Widget passThrough(Widget&& w) {
    return w;
}

int main() {
    Widget w1("w1");
    Widget w2("w2");

    Widget wt1 = passThroughMove(std::move(w1));
    Widget wt2 = passThrough(std::move(w2));

    return 0;
}

It outputs:

Widget created with name: w1.
Widget created with name: w2.
Widget w1 just got moved.
Widget w2 just got copied.

In passThroughMove(Widget&& w), w's type is already rvalue reference, std::move(w) just cast it into rvalue reference again. If I uncomment the TD lines, I can see that of decltype(w) and decltype(std::move(w)) are both Widget &&:

move_parameter.cpp:27:21: error: implicit instantiation of undefined template 'TD<Widget &&>'
    TD<decltype(w)> wType;
                    ^
move_parameter.cpp:28:32: error: implicit instantiation of undefined template 'TD<Widget &&>'
    TD<decltype(std::move(w))> mwType;
                               ^

As both w and std::move(w) are same rvalue reference type, why "return std::move(w)" moves w, while "return w" only copy?

Edit: Thanks for the answers and comments. I got a better understanding now, but not sure if it's accurate. So std::move(w) returns an rvalue reference, just as w itself. But std::move(w) as a function call, it is an rvalue by itself, so it can be moved. While w as a named variable, it is an lvalue by itself, though the type of it is rvalue reference, so it cannot be moved.

2
The short, oversimplified story is that rvalue references are treated as lvalue references unless they're temporaries. std::move just helps pretend that something is a temporary (in fact, its implementation is just return arg).zneak
The type of something is not the same as its value category, and its important not to conflate these things. w has type Widget&&, but its value category must be lvalue, because it is a named expression.Nir Friedman

2 Answers

1
votes

In passThroughMove(Widget&& w), w's type is already rvalue reference, std::move(w) just cast it into rvalue reference again.

So std::move(w) returns an rvalue reference, just as w itself.

No, std::move(w) casts to rvalue, while rvalue references are lvalues.

Both functions passThroughMove and passThrough return by value. They differ, however, in the way they create internally such return value. Internally, passThroughMove creates its return value by move. A new Widget object (the return value) is created by moving into it, that's the effect of std::move on the return value. passThrough on the other hand creates its own return value by copy.

The fact that the assignment

Widget wt2 = passThrough(std::move(w2));

is done from an rvalue does not change the fact that passThrough is forced to create its return value by copy.

In the output of the code you see the effect of the above semantics plus RVO. Without RVO both assignments should result into two additional move-constructions, which are optimized away.

5
votes

The type of an expression is different than the type of a variable, and decltype does both.

decltype(w)

is the the variable w.

decltype((w))

is the type of the expression w (well (w) but those are the same).

If you have a variable of type foo&&, when used in an expression its type is foo& -- it is named, and hence an lvalue.

This makes some sense. foo&& just means it can bind to a temporary. Once it is bound, it has a name and can be used more than once.

Anything that can be used more than once should not be implicitly moved-from.

The only exceptions to this rule that named things are lvalues are the implicit move on return rules. In a few cases, where elision would occur but is blocked for whatever reason, values are implicitly moved. These exceptions do not apply here.