Is
auto x = initializer;
equivalent to
decltype(initializer) x = initializer;
or
decltype((initializer)) x = initializer;
or neither?
Is
auto x = initializer;
equivalent to
decltype(initializer) x = initializer;
or
decltype((initializer)) x = initializer;
or neither?
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&.
This won't work (and is ugly):
decltype([]() { foo(); }) f = []() { foo(); };
whereas
auto f = []() { foo(); };
will.
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:
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;
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 nameddecltype(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 expressionsOtherwise, 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):
T
gives T
T
gives T&&
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 lvalueT&&
, the expression f()
is an xvalueT
, the expression f()
is a prvalueand 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 prvalueReference-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 = prvaluedecT
is T&&
if LXR = xvaluedecT
is T&
if LXR = lvalueEXPR_DEC_TYPE
returns decT
which is the same as the declared return type.
initializer
is an array then decltype(x)
or decltype((x))
don't work simply on it. However auto
will be deduced to a
pointer.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.