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?
decltype returnsIf what we pass to decltype is the name of a variable (e.g. decltype(x) above) or function or denotes a member of an object ( decltype x.i ), then the result is the type of whatever this refers to. As the example of decltype(y) above shows, this includes reference, const and volatile specifiers.
The decltype type specifier yields the type of a specified expression. The decltype type specifier, together with the auto keyword, is useful primarily to developers who write template libraries. Use auto and decltype to declare a template function whose return type depends on the types of its template arguments.
Decltype keyword in C++ Decltype stands for declared type of an entity or the type of an expression. It lets you extract the type from the variable so decltype is sort of an operator that evaluates the type of passed expression. SYNTAX : decltype( expression )
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.
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