Reputation: 2046
I've read some of the prior top answers as well as Stroustrup's "The C++ Programming Language" and "Effective Modern C++" but I'm having trouble really understanding the distinction between the lvalue/rvalue aspect of an expression vs its type. In the introduction to "Effective Modern C++" it says:
A useful heuristic to determine whether an expression is an lvalue is to ask if you can take its address. If you can, it typically is. If you can't, it's usually an rvalue. A nice feature of this heuristic is that it helps you remember that the type of an expression is independent of whether the expression is an lvalue or rvalue ... It's especially important to remember this when dealing with a parameter of rvalue reference type, because the parameter itself is an lvalue.
I'm not understanding something because I don't understand why if you have an rvalue reference type parameter you need to actually cast it to an rvalue via std::move()
to make it eligible to be moved. Even if the parameter (all parameters) is an lvalue the compiler knows its type is an rvalue reference so why the need to tell the compiler that it can be moved? It seems redundant but I guess I am not understanding the distinction between the type of an expression vs its lvalue/rvalue nature (not sure of the right terminology).
Edit:
To follow-up to some of the answers/comments below what's still not clear is why in doSomething()
below I would need to wrap the parameter in std::move()
to get it to bind to an rvalue reference and resolve to the 2nd version of doSomethingElse()
. I understand that if this were to implicitly happen it would be bad because the parameter would have been moved from and one could inadvertently use it after this. It seems like the the rvalue reference type nature of the parameter is meaningless within the function as its only purpose was to bind to resolve to the right version of the function given an rvalue was passed in as an argument.
Widget getWidget();
void doSomethingElse(Widget& rhs); // #1
void doSomethingElse(Widget&& rhs); // #2
void doSomething(Widget&& rhs) {
// will call #1
doSomethingElse(rhs);
// will call #2
doSomethingElse(std::move(rhs));
}
int main() {
doSomething(getWidget());
}
Upvotes: 2
Views: 444
Reputation: 172924
I don't understand why if you have an rvalue reference type parameter you need to actually cast it to an rvalue via
std::move()
to make it eligible to be moved.
As the quotes said, types and value categories are different things. A parameter is always an lvalue, even its type is an rvalue-reference; we have to use std::move
to bind it to an rvalue-reference. Suppose we allow the compiler to do it implicitly, like the following code snippet,
void foo(std::string&& s);
void bar(std::string&& s) {
foo(s);
// continue to use s...
// oops, s might have been moved
foo(std::string{}); // this is fine;
// the temporary will be destroyed after the full expression and won't be used later
}
So we have to use std::move
explicitly, to tell the compiler that we know what we're trying to do.
void bar(std::string&& s) {
foo(std::move(s));
// we know that s might have been moved
}
Upvotes: 3
Reputation: 385174
I think you've actually grasped the distinction between type and value category, so I'll focus on two specific claims/queries:
You need to actually cast it to an rvalue via std::move() to make it eligible to be moved
Sort of, but not really. Coercing the expression that names or refers to your object into an rvalue allows us to trigger, during overload resolution, the function overload that takes a Type&&
. It is convention that we do this when we want to transfer ownership, but that's not quite the same as making it "eligible to be moved" because a move may not be what you end up doing. This is kind of nitpicking in a sense, though I think it's important to understand. Because:
Even if the parameter (all parameters) is an lvalue the compiler knows its type is an rvalue reference so why the need to tell the compiler that it can be moved?
Unless you write std::move(theThing)
, or the thing is a temporary (an rvalue already) then it's not an rvalue, and so it cannot bind to an rvalue reference. That's how it's all designed and defined. It's deliberately made that way so that an lvalue expression, an expression that names a thing, a thing that you haven't written std::move()
around, will not bind to an rvalue reference. And so either your program will not compile or, if available, overload resolution will instead pick a version of the function that takes maybe a const Type&
— and we know that there can be no ownership transfer involved with that.
tl;dr: the compiler doesn't know that its type is an rvalue reference, because it isn't one. Much like you can't do int& ref = 42
, you can't do int x = 42; int&& ref = x;
. Otherwise, it'd try to move everything! The whole point is to make certain kinds of references only work with certain kinds of expression, so that we can use this to trigger calls to copying/moving functions as appropriate with a minimum of machinery at the callsite.
Upvotes: 0
Reputation: 1901
RValue references exist to solve the forwarding problem. Existing type deduction rules in C++ made it impossible to have consistent and sensible move semantics. So, the type system was extended and new rules were introduced to make it more complicated but consistent.
This only makes sense if you look at it through the perspective of the problem that was being solved. Here is a good link dedicated just to explaining RValue references.
Upvotes: 0