What expressions yield a reference type when decltype is applied to them?
I was reading C++ Primer and couldn't quite understand when an expression yields an object type, and when it yields a reference type to the object.
I quote from the book:
- When we apply decltype to an expression that is not a variable, we get the type that > that expression yields.
- Generally speaking, decltype returns a reference type for expressions that yield objects that can stand on the left-hand side of the assignment.
Considering the code below:
int i = 3, *ptr = &i, &ref = i;
decltype(ref + 0) j;
In the above code, the expression "ref + 0" results in an inherent operation of addition of value of the object that ref refers to, i and 0. Hence, going by the first rule the expression yields an int type. But going by the second rule, as the expression yields the type of an object that can stand on the left hand side of an assignment (in this case int), shouldn't the decltype yield a ref to int(int&) type?
The book also says, for the following code
decltype(*ptr) k;
k has type int& and not int, the type which the expression results in.
It also says that for an assignment expression like in code below
decltype(a = b) l;
l would have the type of reference to object on the left hand side of the assignment operation.
How would we know which expressions yield the object type and which yield the reference to the object type?
It is not easy to understand these concepts without getting formal. The primer probably does not want to confuse you and avoids introducing terms such as "lvalue", "rvalue", and "xvalue". Unfortunately, these are fundamental in order to understand how decltype
works.
First of all, the type of an evaluated expression is never a reference type, nor a top-level const
-qualified type for non-class types (e.g. int const
or int&
). If the type of an expression turns out to be int&
or int const
, it gets immediately transformed into int
prior to any further evaluation.
This is specified in paragraphs 5/5 and 5/6 of the C++11 Standard:
5 If an expression initially has the type “reference to T” (8.3.2, 8.5.3), the type is adjusted to
T
prior to any further analysis. The expression designates the object or function denoted by the reference, and the expression is an lvalue or an xvalue, depending on the expression.6 If a prvalue initially has the type “cv T,” where
T
is a cv-unqualified non-class, non-array type, the type of the expression is adjusted toT
prior to any further analysis.
So much for expressions. What does decltype
do? Well, the rules that determine the result of decltype(e)
for a given expression e
are specified in paragraph 7.1.6.2/4:
The type denoted by
decltype(e)
is defined as follows:— if
e
is an unparenthesized id-expression or an unparenthesized class member access (5.2.5),decltype(e)
is the type of the entity named bye
. If there is no such entity, or ife
names a set of overloaded functions, the program is ill-formed;— otherwise, if
e
is an xvalue,decltype(e)
isT&&
, whereT
is the type ofe
;— otherwise, if
e
is an lvalue,decltype(e)
isT&
, whereT
is the type ofe
;— otherwise,
decltype(e)
is the type ofe
.The operand of the
decltype
specifier is an unevaluated operand (Clause 5).
This can indeed sound confusing. Let's try to analyze it part by part. First of all:
— if
e
is an unparenthesized id-expression or an unparenthesized class member access (5.2.5),decltype(e)
is the type of the entity named bye
. If there is no such entity, or ife
names a set of overloaded functions, the program is ill-formed;
This is simple. If e
is just the name of a variable and you do not put it within parentheses, then the result of decltype
is the type of that variable. So
bool b; // decltype(b) = bool
int x; // decltype(x) = int
int& y = x; // decltype(y) = int&
int const& z = y; // decltype(z) = int const&
int const t = 42; // decltype(t) = int const
Notice, that the result of decltype(e)
here is not necessarily the same as the type of the evaluated expression e
. For instance, the evaluation of the expression z
yields a value of type int const
, not int const&
(because by paragraph 5/5 the &
gets stripped away, as we have seen previously).
Let's see what happens when the expression is not just an identifier:
— otherwise, if
e
is an xvalue,decltype(e)
isT&&
, whereT
is the type ofe
;
This is getting complicated. What is an xvalue? Basically, it is one of the three categories an expression can belong to (xvalue, lvalue, or prvalue). An xvalue is normally obtained when invoking a function with a return type which is an rvalue reference type, or as the result of a static cast to an rvalue reference type. The typical example is a call to std::move()
.
To use the wording from the Standard:
[ Note: An expression is an xvalue if it is:
— the result of calling a function, whether implicitly or explicitly, whose return type is an rvalue reference to object type,
— a cast to an rvalue reference to object type,
— a class member access expression designating a non-static data member of non-reference type in which the object expression is an xvalue, or
— a
.*
pointer-to-member expression in which the first operand is an xvalue and the second operand is a pointer to data member.In general, the effect of this rule is that named rvalue references are treated as lvalues and unnamed rvalue references to objects are treated as xvalues; rvalue references to functions are treated as lvalues whether named or not. —end note ]
So for instance, the expressions std::move(x)
, static_cast<int&&>(x)
, and std::move(p).first
(for an object p
of type pair
) are xvalues. When you apply decltype
to an xvalue expression, decltype
appends &&
to the type of the expression:
int x; // decltype(std::move(x)) = int&&
// decltype(static_cast<int&&>(x)) = int&&
Let's continue:
— otherwise, if
e
is an lvalue,decltype(e)
isT&
, whereT
is the type ofe
;
What is an lvalue? Well, informally, lvalue expression are expressions which denote objects that can be repeatably referenced in your program - for instance variables with a name and/or objects you can take the address of.
For an expression e
of type T
that is an lvalue expression, decltype(e)
yields T&
. So for instance:
int x; // decltype(x) = int (as we have seen)
// decltype((x)) = int& - here the expression is parenthesized, so the
// first bullet does not apply and decltype appends & to the type of
// the expression (x), which is int
A function call for a function whose return type is T&
is also an lvalue expression, so:
int& foo() { return x; } // decltype(foo()) = int&
Finally:
— otherwise,
decltype(e)
is the type ofe
.
If the expression is not an xvalue nor an lvalue (in other words, if it is a prvalue), the result of decltype(e)
is simply the type of e
. Unnamed temporaries and literals are prvalues. So for instance:
int foo() { return x; } // Function calls for functions that do not return
// a reference type are prvalue expressions
// decltype(foo()) = int
// decltype(42) = int
Let's apply the above to the examples from your question. Given these declarations:
int i = 3, *ptr = &i, &ref = i;
decltype(ref + 0) j;
decltype(*ptr) k;
decltype(a = b) l;
The type of j
will be int
, because operator +
returns a prvalue of type int
. The type of k
will be int&
, because the unary operator *
yields an lvalue (see paragraph 5.3.1/1). The type of l
is also int&
, because the result of operator =
is an lvalue (see paragraph 5.17/1).
Concerning this part of your question:
But going by the second rule, as the expression yields the type of an object that can stand on the left hand side of an assignment (in this case int), shouldn't the decltype yield a ref to int(int&) type?
You probably misinterpreted that passage from the book. Not all objects of type int
can be on the left side of an assignment. For instance, the assignment below is illegal:
int foo() { return 42; }
foo() = 24; // ERROR! foo() is a prvalue expression, cannot be on the left
// side of an assignment
Whether or not an expression can appear on the left side of an assignment (notice, that we are talking about the built-in assignment operator for fundamental data types here) depends on the value category of that expression (lvalue, xvalue, or prvalue), and the value category of an expression is independent from its type.
For expressions, as in your examples decltype will provide a reference type if the argument is lvalue.
7.1.6.2p4:
The type denoted by decltype(e) is defined as follows:
— if e is an unparenthesized id-expression or an unparenthesized class member access (5.2.5), decltype(e) is the type of the entity named by e. If there is no such entity, or if e names a set of overloaded functions, the program is ill-formed;
— otherwise, if e is an xvalue, decltype(e) is T&&, where T is the type of e;
— otherwise, if e is an lvalue, decltype(e) is T&, where T is the type of e;
— otherwise, decltype(e) is the type of e.
The operand of the decltype specifier is an unevaluated operand (Clause 5).
[ Example:
const int&& foo();
int i;
struct A { double x; };
const A* a = new A();
decltype(foo()) x1 = i; // type is const int&&
decltype(i) x2; // type is int
decltype(a->x) x3; // type is double
decltype((a->x)) x4 = x3; // type is const double&
—end example ]