12

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
  • 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). Commented Oct 22, 2016 at 2:29
  • 3
    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. Commented Oct 22, 2016 at 4:18

3 Answers 3

9

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.

Sign up to request clarification or add additional context in comments.

11 Comments

Do you mean, in passThroughMove() it returned a temporary returned from std::move(), so it got moved?
Widget passThroughMove(Widget&& w) { //TD<decltype(w)> wType; //TD<decltype(std::move(w))> mwType; auto wm = std::move(w); //return std::move(w); return wm; }
... Can't format code in comments ... But I also tried "auto wm = std::move(w);" then "return wm", it was still moved rather than copied.
Meh. If you tell me "the type of an rvalue reference is an lvalue reference", I would be like "wat?"
Note that as of P1825R0 and as per the accoring updates to [class.copy.elision]/3, this particular example (passThrough(Widget&& w)) has also become an exception, and the lvalue w, in overload resolution when selecting a constructor for the return statement, may be considered as if it were an rvalue, thus choosing the move constructor over the copy constructor.
|
1

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.

2 Comments

> std::move(w) casts to rvalue, while rvalue references are lvalues. This is gold.
There's already a comment to this effect, but it's still worth commenting. "rvalue references are lvalues" This language has gone off the rails. There's no other explanation.
1

In C++20, you no longer need the std::move. The program now prints

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

This is because they made a special rule for return statements:

A function return statement will automatically invoke a move rather than a copy in some situations. This proposal slightly extends those situations.

This was buried in another comment, but needed spelling out in an answer.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.