auto
and decltype
Explained,
by Thomas Becker
about me
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:
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 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 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 parameterbecause 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.
|