Search code examples
c++c++11move-semanticsrvalue-reference

Scott Meyers on Rvalueness


I watched Scott Meyers's extremely informative video on Universal References, in which I learned most of what I know about Rvalue references, moving, and forwarding. At one point he was talking about rvalueness as opposed to the type of a variable, and he said something to the effect of "rvalueness is independent of type".

I understand that you can have a method like this:

void func(MyType&& rRef)
{
    // Do domething with rRef...
}

and that here rRef is an lvalue because it can be identified, its address can be taken, etc., even though its type is MyType&&.

But an rvalue cannot be any type, can it? I mean, it can only be a MyType&&, right? In that sense I thought type is not entirely independent of rvalueness. Maybe I'm missing something.

Updated: My point can be made clearer like this. If in func() I call one of two overloaded functions defined as

void gunc(MyType&& rRef)
{
   // ...
}

void gunc(MyType& lRef)
{
   // ...
}

i.e. either by calling gunc(std::move(rRef)) or gunc(rRef), it seems that the type of the resulting expression between parenthesis is not independent of rvalueness.


Solution

  • The type of an expression does not have any traces of references. So if for a moment we assume that references could have reference type, then we would have the following

    int a = 0;
    int &ra = a;
    
    int c = a + 42;
    int d = ra + 42;
    

    In the above, the expression a would have type int, and the expression ra would have type int&. I think that in nearly all the rules of the spec that relate expressions to type, for example rules that say "expression E must be of type X", we would have to add "... or reference to type X" (think about cast operators). So my educated guess is that this would be too much of a burden to be useful.


    C++ has the following types

    • static type of an expression

    Just called "type of the expression" (if not otherwise specified that the dynamic one is meant). This is a property of expressions that designate the type of expressions abstracted away of what the expression refers to at compile time. For example if a refers to an int& or int variable, or is a literal 0, all those expression have type int.

    • dynamic type of an lvalue expression

    This is the type that the non-base-class object that an lvalue expression refers to has.

    ofstream fs("/log");
    ostream &os = fs;
    

    In this, os has the static type ostream and the dynamic type ofstream.

    • type of an object or reference

    This is the type that an object or reference actually has. An object always has a single type and its type never changes. But what object exists at what location is something only known at runtime, so generally, "type of an object" is a runtime thing too

    ostream *os;
    if(file)
      os = new ofstream("/log");
    else
      os = new ostringstream;
    

    The type of the object denoted by *os (and the dynamic type of the lvalue *os aswell) is known only at runtime

    int *p = new int[rand() % 5 + 1];
    

    Here, the type of the array that was created by operator new is only known at runtime too, and (thanksfully) does and can not escape to the static C++ type system. The infamous aliasing rule (that forbids reading objects from incompatible lvalues, roughly speaking) speaks of "dynamic type" of objects, presumably because it wants to highlight that runtime concerns are of interests. But strictly speaking, saying "dynamic type" of an object is weird, because an object doesn't have a "static type".

    • declared type of a variable or member

    This is the type that you gave in a declaration. In relation to the type of an object or the type of an expression, this sometimes can be subtly different

    struct A {
      A() { }
      int a;
    };
    
    const A *a = new const A;
    volatile const A *va = a;
    

    Here, the expression a->a has type const int, but the declared type of the member that a->a resolves to has type int (the member entity). The type of the object denoted by a->a has type const int, because we created a const A object with the new expression, and therefore all non-static data members are implicitly const subobjects. In va->a, the expression has type volatile const int a, and the declared type of the variable still has type int and the type of the object referred to still has type const int.


    When you say "type of a" and you declared "a" as "int &a;" you therefor always have to say what you mean by "type of a". Do you mean the expression? Then "a" has type int. It can even become nastier. Imagine "int a[10];". Here the expression "a" has type int* or int[10] depending on whether you consider the array to pointer conversion to have taken place in your expression or not, when you ask for the "type of a". If you ask for the type of the variable referred to by "a", then the answer uniquely is int and int[10] respectively.


    So what type can an rvalue be of? Rvalues are expressions.

    int &&x = 0;
    int y = std::move(x);
    int z = x;
    

    Here, we have rvalues 0, and std::move(x). Both rvalues have type int. The expression x appearing in the initializer for z is an lvalue, even though it refers to the same object that the rvalue std::move(x) refers to.


    Your last point in your question about the overloaded function called with an rvalue or lvalue respectively is interesting. Just because rvalue references are written as int && does not mean that rvalues have type int. They are called rvalue references because you can initialize them with rvalues and the language prefers that initialization over initializing an lvalue reference with an rvalue.


    Also, it may be useful to see expressions in name-form that are rvalues

    enum A { X };
    template<int Y> struct B { };
    

    If you use X or Y, they are rvalues. But those cases are the only one that I can think of.