The relationship between auto and decltype

2019-01-21 04:38发布

问题:

Is

auto x = initializer;

equivalent to

decltype(initializer) x = initializer;

or

decltype((initializer)) x = initializer;

or neither?

回答1:

decltype also considers whether the expression is rvalue or lvalue .

Wikipedia says,

The type denoted by decltype can be different from the type deduced by auto.

#include <vector>
int main()
{
    const std::vector<int> v(1);
    auto a = v[0];        // a has type int
    decltype(v[0]) b = 1; // b has type const int&, the return type of
                        // std::vector<int>::operator[](size_type) const
    auto c = 0;           // c has type int
    auto d = c;           // d has type int
    decltype(c) e;        // e has type int, the type of the entity named by c
    decltype((c)) f = c;  // f has type int&, because (c) is an lvalue
    decltype(0) g;        // g has type int, because 0 is an rvalue
}

That pretty much explains the imporant difference. Notice decltype(c) and decltype((c)) are not same!

And sometime auto and decltype works together in a cooperative way, such as in the following example (taken from wiki, and modified a bit):

int& foo(int& i);
float foo(float& f);

template <class T>
auto f(T& t) −> decltype(foo(t)) 
{
  return foo(t);
}

Wikipedia further explains the semantics of decltype as follows:

Similarly to the sizeof operator, the operand of decltype is unevaluated. Informally, the type returned by decltype(e) is deduced as follows:

  • If the expression e refers to a variable in local or namespace scope, a static member variable or a function parameter, then the result is that variable's or parameter's declared type
  • If e is a function call or an overloaded operator invocation, decltype(e) denotes the declared return type of that function
  • Otherwise, if e is an lvalue, decltype(e) is T&, where T is the type of e; if e is an rvalue, the result is T

These semantics were designed to fulfill the needs of generic library writers, while at the same time being intuitive for novice programmers, because the return type of decltype always matches the type of the object or function exactly as declared in the source code. More formally, Rule 1 applies to unparenthesized id-expressions and class member access expressions. For function calls, the deduced type is the return type of the statically chosen function, as determined by the rules for overload resolution. Example:

const int&& foo();
int i;
struct A { double x; };
const A* a = new A();
decltype(foo()) x1; // type is const int&&
decltype(i) x2; // type is int
decltype(a->x) x3; // type is double
decltype((a->x)) x4; // type is const double&

The reason for the difference between the latter two invocations of decltype is that the parenthesized expression (a->x) is neither an id-expression nor a member access expression, and therefore does not denote a named object.Because the expression is an lvalue, its deduced type is "reference to the type of the expression", or const double&.



回答2:

This won't work (and is ugly):

decltype([]() { foo(); }) f = []() { foo(); };

whereas

auto f = []() { foo(); };

will.



回答3:

It depends. auto and decltype serve different purposes so they don't map one-to-one.

The rules for auto are the easiest to explain because they are the same as for template parameter deduction. I won't expand on them here, but note that auto& and auto&& are also some possible uses!

decltype however has several cases, some of which you have illustrated above (information and quotes taken from n3290, 7.1.6.2 Simple type specifiers [dcl.type.simple]) that I separate into two categories:

  • when using what the Standard calls "an unparenthesized id-expression or an unparenthesized class member access"
  • the rest!

Informally, I'd say that decltype can operate on either names (for the first case) or expressions. (Formally and according to the grammar decltype operates on expressions, so think of the first case as a refinement and the second case as a catch-all.)

When using a name with decltype, you get the declared type of that entity. So for instance decltype(an_object.a_member) is the type of the member as it appears in the class definition. On the other hand, if we use decltype( (an_object.a_member) ) we find ourselves in the catch-all case and we're inspecting the type of the expression as it would appear in code.

Accordingly, how to cover all the cases of your questions:

int initializer;
auto x = initializer; // type int
// equivalent since initializer was declared as int
decltype(initializer) y = initializer;


enum E { initializer };
auto x = initializer; // type E
// equivalent because the expression is a prvalue of type E
decltype( (initializer) ) y = initializer;


struct {
    int const& ializer;
} init { 0 };
auto x = init.ializer; // type int
// not equivalent because declared type is int const&
// decltype(init.ializer) y = init.ializer;
// not equivalent because the expression is an lvalue of type int const&
// decltype( (init.ializer) ) y = init.ializer;


回答4:

auto

auto is simple: it will give the same type as by-value template parameter deduction. auto works uniformly on expressions.

template <class T>
void deduce(T x);

int &refint();
std::string str();
std::string const conststr();

auto i1 = 1; // deduce(1) gives T=int so int i1
auto i2 = i1; // deduce(i1) gives T=int so int i2
auto i3 = refint(); // deduce(refint()) gives T=int so int i3
const auto ci1 = i1; // deduce(i1) gives T=int so const int ci1
auto i4 = ci1; // deduce(ci1) gives T=int so int i4

auto s1 = std::string(); // std::string s1
auto s2 = str(); // std::string s2
auto s3 = conststr(); // std::string s3

In C++ expressions cannot have reference type (refint() has type int not int&).

Note that the lvalueness of the expression is not an issue on an expression on the right (on the right of equal sign, or something which is being copied in general). The rvalue 1 is treated like the lvalues i1 and refint().

For by value parameters (that is, non reference parameters), not only lvalue to rvalue conversion is applied, but also array to pointer conversions. const is ignored.

decltype

decltype is a very useful feature with an horrible interface:

decltype acts differently on some expressions defined in term of name lookup and other expressions! This is the type of features that make people hate C++.

decltype of something named

decltype(entity) will do name lookup and give the declared type of the entity. (entity can be an unqualified or qualified identifier, or a member access such as expr.identifier.)

decltype(f(args)) will do name lookup and overload resolution and give the declared return type of the function, not the type the expression:

extern decltype(refint()) ri1; // int &ri1

So now I can check my understanding of the language with decltype:

template <class T, class U>
struct sametype {};

template <class T>
struct sametype<T,T> {typedef int same;};

sametype<T,U>::same exists iff T and U are exactly the same type.

sametype<decltype (i1), int>::same check_i1;
sametype<decltype (i2), int>::same check_i2;
sametype<decltype (i3), int>::same check_i3;
sametype<decltype (i4), int>::same check_i4;

sametype<decltype (ci1), const int>::same check_ci1;
sametype<decltype (ir1), int&>::same check_ir1;

sametype<decltype (s1), std::string>::same check_s1;
sametype<decltype (s2), std::string>::same check_s2;
sametype<decltype (s3), std::string>::same check_s3;

compiles fine, so I was not wrong!

decltype of other expressions

Otherwise, where expr is not defined in term of name lookup (not one of the above cases), such as the expression (expr), decltype implements a distinct feature (but the C++ designers would not spent another keywords on it.)

decltype(expr) will give the type of the expression decorated with its lxrvalueness (lvalue/xvalue/prvalue-ness):

  • prvalue (pure rvalue) of type T gives T
  • xvalue of type T gives T&&
  • lvalue of type T gives T&

This is the reciprocal of function call rule: if f is a function with return type

  • T&, the expression f() is an lvalue
  • T&&, the expression f() is an xvalue
  • naked type (pure object type, non reference) T, the expression f() is a prvalue

and also for casts: for naked type (pure object type, non reference) T

  • (T&)expr is a lvalue
  • (T&&)expr is an xvalue
  • (T)expr is a prvalue

Reference-ness is the encoding of lxrvalueness into types. decltype does this encoding to save and transfer the lxrvalueness of things.

This is useful when you want to alias an expression: the lxrvalueness of an expression expr that is a function call (either normal f(args) or with operator syntax like a @ b) is the same as the lxrvalueness of alias() declared as decltype(expr) alias();

This can be used for pure forwarding in generic code:

// decorated type of an expression
#define EXPR_DEC_TYPE(expr) decltype((expr))

int i;
int &ri = i;
int fi();
int &fri();

EXPR_DEC_TYPE(i) alias_i = i; // int &
EXPR_DEC_TYPE(ri) alias_ri = ri; // int &

EXPR_DEC_TYPE(fi()) alias_fi(); // int alias_fi()
EXPR_DEC_TYPE(fri()) alias_fri(); // int &alias_fri()

Note that EXPR_DEC_TYPE(foo()) equals declexpr(foo()) by design (in most cases), but the computations are different:

  • declexpr(foo(args)) does name lookup for foo, does overload resolution, finds the declaration, returns the exact return type declared, end of story

  • EXPR_DEC_TYPE(foo(args)) finds the type of the declaration then computes

    • the type T of the expression which is the return type naked (without
      reference)

    • the lxrvalueness LXR of the expression according to the referenceness of the declared return type: lvalue for reference, xvalue of r-reference...

    then it decorates the type T with LXR to obtain the decT type:

    • decT is T if LXR = prvalue
    • decT is T&& if LXR = xvalue
    • decT is T& if LXR = lvalue

    EXPR_DEC_TYPE returns decT which is the same as the declared return type.



回答5:

  1. If initializer is an array then decltype(x) or decltype((x)) don't work simply on it. However auto will be deduced to a pointer.
  2. If initializer is a function then applying decltype(fp) will deduce to the function type however, auto will deduce to its return type.

So in general auto cannot be considered as a replacement of any decltype() version you asked.