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.
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
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
.
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
.
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".
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.
I think you are leaving off part of his quote:
A final point is worth bearing in mind: the lvalueness or rvalueness of an expression is independent of its type.
He explains it here. His main point was this:
The type of an expression does not tell you whether it is an lvalue or an rvalue.
And in his concluding remarks:
In a type declaration, “&&” indicates either an rvalue reference or a universal reference – a reference that may resolve to either an lvalue reference or an rvalue reference. Universal references always have the form T&& for some deduced type T.
Reference collapsing is the mechanism that leads to universal references (which are really just rvalue references in situations where reference-collapsing takes place) sometimes resolving to lvalue references and sometimes to rvalue references. It occurs in specified contexts where references to references may arise during compilation. Those contexts are template type deduction, auto type deduction, typedef formation and use, and decltype expressions.
The type T
is used here to mean any type. It could be int&&
, or double&&
, or Widget&&
- it doesn't matter.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With