Step 6 of 9
Value categories and reference collapsing
Move semantics and perfect forwarding rest on one idea the standard calls value categories. Every expression has a type (int, std::string) and a category, which answers two questions: does it have an identity (is it a particular object you could refer to again)? And may its resources be taken (moved from)?
| Category | Identity? | Movable? | Examples |
|---|---|---|---|
| lvalue | yes | no | a variable s, *p, v[0], a call returning T& |
| prvalue ("pure rvalue") | no | yes | 42, a + b, std::string("x"), a call returning T by value |
| xvalue ("expiring value") | yes | yes | std::move(s), a call returning T&& |
Two group names cover them: rvalues are prvalues and xvalues (anything you may move from), and glvalues are lvalues and xvalues (anything with an identity). Overload resolution uses the category: an rvalue prefers a T&& parameter, an lvalue binds to T& or const T&.
#include <iostream>
#include <string>
#include <utility>
std::string make() { return "temp"; }
void which(const std::string&) { std::cout << "lvalue\n"; }
void which(std::string&&) { std::cout << "rvalue\n"; }
int main() {
std::string s = "named";
which(s); // lvalue
which(make()); // prvalue
which(std::move(s)); // xvalue
which(s + "!"); // prvalue
std::string&& r = make();
which(r); // a NAMED rvalue reference is an lvalue
}
lvalue
rvalue
rvalue
rvalue
lvalue
The last line surprises everyone once. r's type is std::string&&, but the expression r is a name, so its category is lvalue. Anything with a name is an lvalue. That's why a function taking std::string&& s still has to write std::move(s) to pass it on as an rvalue.
Reference collapsing
In a template, T&& with a deduced T is a forwarding reference. Pass an lvalue std::string and T is deduced as std::string&, which makes the parameter std::string& &&. You can't write a reference to a reference yourself, but when one appears through templates or aliases it collapses:
| Combined | Collapses to |
|---|---|
& &, & &&, && & |
& |
&& && |
&& |
If either side is &, the result is &. So an lvalue argument gives an lvalue reference parameter, and an rvalue argument (where T is plain std::string) gives std::string&&. std::forward<T>(x) then casts x back to an rvalue only when T isn't a reference, which is exactly "only when the caller passed an rvalue". You can ask the same question directly with std::is_lvalue_reference_v<T>.
Your turn: write category(x), which returns "lvalue" or "rvalue" for whatever the caller passed, and keep(out, x), which adds x to the vector, copying lvalues and moving rvalues. The tests use a Tracker type that counts its copies and moves.
Previous: Constraining templates with concepts Next: SFINAE and enable_if