What expressions yield a reference type when decltype is applied to them?

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 to T 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 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).

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 by e. If there is no such entity, or if e 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) is T&&, where T is the type of e;

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) is T&, where T is the type of e;

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 of e.

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.

Leave a Comment