As a rule of thumb, an id-expression is typically an lvalue, although there are some exceptions to this.
The unqualified-id expression bar
inside of foo
is an lvalue of type string
.
Detailed Explanation
bar
is an expression.
Namely, it is an identifier and thus an unqualified-id.
The standard says this in [expr.prim.id.unqual] p3:
The expression is an xvalue if it is move-eligible (see below); an lvalue if the entity is a function, variable, structured binding, data member, or template parameter object; and a prvalue otherwise ([basic.lval]); it is a bit-field if the identifier designates a bit-field.
bar
is not move-eligible, so it is an lvalue, not an rvalue (xvalue or prvalue).
However, as stated, there are cases where unqualified-ids are rvalues.
For example,
concepts (C++20) and enumerators (enum
constants) are prvalues.
Conveniently, "local variables" (specifically, implicitly movable entities) get moved implicitly in throw
, return
, and co_return
:
std::string foo() {
std::string s = "awoo";
return s; // Here, s is an xvalue, not an lvalue.
// The return statement doesn't copy s.
}
std::string foo(std::string&& s) {
return s; // Same as above: s is an xvalue, no need to std::move.
}
Note: I am citing the latest working draft, but the rules haven't really changed since C++11.
Rationale
To some, it is surprising and annoying that you need to std::move
everywhere to not have things turn into lvalues.
However, this is necessary to cover the case where you use some rvalue reference (or anything else) multiple times, such as when you have a mixture of copying and moving:
void take(std::string);
void foo(std::string&& s) {
take(s); // first copy
take(std::move(s)); // then move
}
If s
was an rvalue, you would need to "opt out" of move semantics for the first call to take
(std::unmove
?).
Whether one has to move or unmove, they must do something explicitly, and it's better if being forgetful results in unnecessary copies instead of a use-after-move bug.
A way to think about it is that rvalue references give you the right to move from the object (unlike lvalue references), but not the obligation.
string temp1(bar), temp2(bar);
– Emmybar
is an lvalue (its type is an rvalue, weird I know). If you calledstd::move
twice you could run into issues however. – Vociferantbar
is precisely whybar
is an lvalue - so that such double use doesn't give you issues. It only gets treated as an rvalue when you do it explicitly:std::move(bar)
. – Scrannelmove
constructor? I just don't get it. Why do we call it an rvalue reference at all then? The only thing I thought it did was require move construction. But if doesn't even do that... – Emmybar
will be destroyed, that is what you get. However if you then call a function within yours you don't make that guarantee unless you explicitly say it. – Vociferantstd::move
or equivalent), so your function can assume it's OK to move from it. – Granulitestatic_cast<int&&>(42)
is still an rvalue-expression, whereas letint x = 42;
thenstatic_cast<int&>(42)
is an lvalue-expression. – Predestinate