C/C++ Arena

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