Search code examples
c++c++11rvalue-referencevalue-categories

Doesn't rvalue reference mix together type and category?


This post reflects my current level of understanding of a value category.

Value category is a property of an expression. Type is a propetry of a variable.

Let's say we have the following declaration:

int x;

What is x? It is a variable and it is an expression.

Writing a new syntax like this:

int&& ref

instead of

int& ref

makes people think that we've done something related to types, but actually we've done something related to expressions, not types. (We've specified what expressions can be bound to these variables.)

Probably distinguishing between "temporaries" and "non-temporaries" could be done another way, maybe something like this:

void func(int&); // for "non-temporaries"
void func((int&)); // for "temporaries

or some other way, but not fiddling with types.

So my question is: Is it necessary to encode information about an expression category into a syntax for types? What were the reasons for this design decision?


Solution

  • Rvalue references are a distinct type from lvalue references so that they can encode something: the user's specific intent when they created that reference. If you make an rvalue reference, you specifically intend that the object it references can be moved from. That is a separate intent from using an lvalue reference.

    That specific intent is not detectable from just the use of the name; the value category of an rvalue reference variable is lvalue. But that intent is detectable from the type. And the type matters.

    We want to be able to overload functions on this declared intent, to have one overload that can take an object which can be moved from and another that doesn't. This is the basis of the distinction between a copy constructor and a move constructor. C++ overloads functions based on types, so... we must specify this intent at the type level.

    In your func example, if int& and (int&) are the same type, then by the rules of C++ as it currently exist, both of those func functions declare the same function. So you'd need to invent new rules that defines the concept of a "function signature" as being more than just the types involved. You'd have to mess with overload resolution to determine which function gets called. Etc.

    Plus, std::move works (that is, the return value can bind to rvalue references) because the value category of a return value of rvalue reference type is defined to be an xvalue. And xvalues can bind to rvalue references. With your way, this (&) syntax would have to have similar properties. But it couldn't be based on the type, because by definition, it doesn't change the type. So in essence, you have to declare that reference types can have this extra, non-type, information sitting beside a type. This information cannot be queried by any normal interface, unlike type information for an expression which can be queried by decltype.

    Or you can just create a new type and get most of that for free. You still need to investigate how overload resolution works for this new type, and you still have to define rvalue reference binding rules, but the concept of a function signature is unchanged, and you don't need this awkward extra channel of information that sits beside a type.

    The use of a reference for this intent also allows for reference-collapsing rules, which are the basis of "perfect" forwarding: the ability to write a single (template) function that forwards expressions of any value category to a destination in a way that leaves intact whether the destination can copy/move from them.