Is your 401k bleeding money by not offering low-cost index funds? Now there is a way to find out.
GreaterThanZero.com


Page 7 of: C++ auto and decltype Explained, by Thomas Becker   about me  

How decltype Deduces the Type of an Expression: Case 2

Since decltype's case distinction has only two cases, the second case is "everything else," meaning everything that is not a plain, unparenthesized variable, function parameter, or class member access. Typical examples are expressions involving operators, such as x * y. For ease of terminology, I will refer to expressions that fall under Case 2 as complex expressions, as opposed to the simple expressions of Case 1. A trivial way to produce such a complex expression is to take a simple expression and throw parentheses around it, as in (x). So what does decltype do with such a complex expression? The exact formulation of the rule uses the terms lvalue, xvalue, and prvalue, so we must make sure we understand those first. The terms xvalue and prvalue define a partitioning of the set of all rvalues into two subsets. Therefore, the first step is to understand the terms lvalue and rvalue. That's actually much more difficult in C++ than it used to be in C. You can find a reasonable working definition in the introduction to my article about rvalue references. So now that we know what lvalues and rvalues are, how is the set of rvalues partitioned into xvalues and prvalues? Here's the definition:
  • An rvalue is an xvalue if it is one of the following:
    1. A function call where the function's return value is declared as an rvalue reference. An example would be std::move(x).
    2. A cast to an rvalue reference. An example would be static_cast<A&&>(a).
    3. A member access of an xvalue. Example: (static_cast<A&&>(a)).m_x.
  • All other rvalues are prvalues.
We are now in a position to describe how decltype deduces the type of a complex expression.
 
Let expr be an expression that is not a plain, unparenthesized variable, function parameter, or class member access. Let T be the type of expr. If expr is an lvalue, then decltype(expr) is T&. If expr is an xvalue, then decltype(expr) is T&&. Otherwise, expr is a prvalue, and decltype(expr) is T.
 
As you can see, this differs significantly from the way auto works, and it also differs from the way decltype works in Case 1, for things like plain variables. The examples below illustrate these differences.

Let us begin by going through the list of simple expressions that we used in the previous section for Case 1 of decltype's definition. This time, we'll throw a set of parentheses around each of these simple expressions to turn them into complex expressions, to which Case 2 applies. To emphasize the differences, we'll also repeat what decltype does with the original simple expression, and what auto does. decltype on complex expressions is brick, decltype on simple expressions is blue, and auto is green. Note that auto never cares whether the expression is in parentheses or not.

struct S {
  S(){m_x = 42;}
  int m_x;
};

int x;
const int cx = 42;
const int& crx = x;
const S* p = new S();

// (x) has type int, and decltype adds references to lvalues.
// Therefore, x_with_parens_type is int&.
//
typedef decltype((x)) x_with_parens_type;

// x is declared as an int: x_type is int.
//
typedef decltype(x) x_type;

// auto also deduces the type as int: a_p and a are ints.
//
auto a_p = (x);
auto a = x;

// The type of (cx) is const int. Since (cx) is an lvalue,
// decltype adds a reference to that: cx_with_parens_type
// is const int&.
//
typedef decltype((cx)) cx_with_parens_type;

// cx is declared as const int: cx_type is const int.
//
typedef decltype(cx) cx_type;

// auto drops the const qualifier: b_p and b are ints.
//
auto b_p = (cx);
auto b = cx;

// The type of (crx) is const int&1, and it is an lvalue.
// decltype adds a reference. By the C++11 reference
// collapsing rules, that makes no difference. Hence,
// crx_with_parens_type is const int&.
//
typedef decltype((crx)) crx_with_parens_type;

// crx is declared as const int&: crx_type is const int&.
//
typedef decltype(crx) crx_type;

// auto drops the reference and the const qualifier: c_p and c
// are ints.
//
auto c_p = (crx);
auto c = crx;

// S::m_x is declared as int. Since p is a pointer to const,
// the type of (p->m_x) is const int. Since (p->m_x) is an
// lvalue, decltype adds a reference to that. Therefore,
// m_x_with_parens_type is const int&.
//
typedef decltype((p->m_x)) m_x_with_parens_type;

// S::m_x is declared as int: m_x_type is int.
//
typedef decltype(p->m_x) m_x_type;

// auto sees that p->m_x is const, but it drops the const
// qualifier. Therefore, d_p and d are ints.
//
auto d_p = (p->m_x);
auto d = p->m_x;
Now let's do some examples of expressions that really are complex, in the sense that they involve operators or function calls. We'll begin with some function and unary operator calls.
const S foo();
const int& foobar();
std::vector<int> vect = {42, 43};

// foo() is declared as returning const S. The type of foo()
// is const S. Since foo() is a prvalue, decltype does not
// add a reference. Therefore, foo_type is const S.
//
// Note: we had to use the user-defined type S here instead of int,
// because C++ does not allow us to return a basic type as const.
// (Ok, it does allow it, but the const would be ignored.)
//
typedef decltype(foo()) foo_type;

// auto drops the const qualifier: a is an S.
//
auto a = foo();

// The type of foobar() is const int&1, and it is an lvalue. 
// Therefore, decltype adds a reference. By the C++11 reference
// collapsing rules, that makes no difference. Therefore,
// foobar_type is const int&.
//
typedef decltype(foobar()) foobar_type;

// auto drops the reference and the const qualifier: b is
// an int.
//
auto b = foobar();

// The type of vect.begin() is std::vector<int>::iterator.
// Since vect.begin() is a prvalue, no reference
// is added. Therefore, iterator_type is
// std::vector<int>::iterator.
//
typedef decltype(vect.begin()) iterator_type;

// auto also deduces the type as std::vector<int>::iterator,
// so iter has type std::vector<int>::iterator.
//
auto iter = vect.begin();

// std::vector<int>'s operator[] is declared to have return
// type int&. Therefore, the type of the expression vect[0]
// is int&1. Since vect[0] is an lvalue, decltype adds a
// reference. By the C++11 reference collapsing rules,
// that makes no difference. Therefore, first_element has
// type int&.  
//
decltype(vect[0]) first_element = vect[0];

// second_element has type int, because auto drops the reference.
//
auto second_element = vect[1];

In the last example above, the first element of the vector can be modified through the reference first_element. The second element cannot be modified through second_element, because the latter is not a reference. This demonstrates how an incomplete understanding of the workings of auto and decltype could lead to coding errors that don't show up until runtime.

Finally, here are some examples of binary and ternary operators:
int x = 0;
int y = 0;
const int cx = 42;
const int cy = 43;
double d1 = 3.14;
double d2 = 2.72;

// The type of the product is int, and the product
// is a prvalue. Therefore, prod_xy_type is an int.
//
typedef decltype(x * y) prod_xy_type;

// auto also deduces the type as int: a is an int.
//
auto a = x * y;

// The type of the product is int (not const int!),
// and the product is a prvalue. Therefore, prod_cxcy_type
// is an int.
//
typedef decltype(cx * cy) prod_cxcy_type;

// same for auto: b is an int.
//
auto b = cx * cy;

// The type of the expresson is double, and the expression
// is an lvalue. Therefore, a reference is added, and
// cond_type is double&.
//
typedef decltype(d1 < d2 ? d1 : d2) cond_type;

// The type of the expression is double, so c is a double.
//
auto c = d1 < d2 ? d1 : d2;

// The type of the expresson is double. The expression
// is a prvalue, because in order to accomodate the
// promotion of x to a double, a temporary has to be
// created. Therefore, no reference is added, and
// cond_type_mixed is double.
//
typedef decltype(x < d2 ? x : d2) cond_type_mixed;

// The type of the expression is double, so d is a double.
//
auto d = x < d2 ? x : d2;
Note that in the last example, you couldn't have just written
auto d = std:min(x, dbl); // error: ambiguous template parameter
because std::min requires its arguments to be of the same type. More on that in the next section.

1There is an alternate way to derive the same result for examples like this. The C++ Standard contains the following clause (5/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." One may argue that applying decltype constitutes "further analysis," and therefore, the types of our expressions (crx), foobar(), and vect[0] have already been stripped of the reference. However, since decltype adds a reference in all these cases, the end result is the same whether or not one believes that 5/5 applies.