Usage and advantage of the decltype specifier
As C++ templates and generic programming become popular, programmers find it sometimes difficult to express the type for a variable or function. People are in urgent need of a mechanism to automatically deduce types for expressions. This new programming mechanism didn't appear until the appearance of decltype.
Decltype solves the problem through getting the type or derived type of an expression and acting as the type specifier of another expression whose type needs to be deduced. Here is an example:
int i;
decltype (i) j = 1;
In this example, decltype(i) gets the type of i, which is int. decltype(i) is the type specifier of variable j, so the type of j is also int.
In some scenarios, decltype can improve the readability and maintainability of programs. See the following example:
vector<int> vec;
for (vector<int>::iterator i = vec.begin(); i < vec.end(); i++)
{
...
}
With decltype, the program can be rewritten as follows:
vector<int> vec;
typedef decltype(vec.begin()) vectype;
for (vectype i = vec.begin(); i < vec.end(); i++)
{
...
}
In this example, we use the combination of typedef and decltype to make vectype equivalent to vector<int>:: iterator. If vector<int>::iterator appears several times in the program, using decltype will greatly improve the readability and maintainability. Another usage of decltype is in trailing return types. For details, see article Introduction to the C++11 feature: trailing return types.
Decltype and templates
We can use decltype in templates to further improve the generic capabilities of templates. For example:
template<typename T, typename U> void sum(T t, U u, decltype(t+u) & s){
s= t+u;
}
int main(){
char var1 = 1;
int var2 = 2;
float var3= 1.0;
double var4 = 2.5;
int var5;
double var6;
sum(var1, var2, var5); // decltype(t+u) is int
sum(var3, var4, var6); // decltype(t+u) is double
}
In this example, decltype(t+u) is not determined until the template instantiation. With decltype, we don't need to specify the type of “t+u” in the template definition. However, a limitation is that users must decide the type of “t+u” in the template instantiation. We can use trailing return types to do further improvement for this program. One consideration is that if “t+u” is not a valid expression during the template instantiation; for example, t and u are arrays, the compiler will issue an error message. To avoid such kind of error, you need to provide other sum functions to support the add operation.
The deduction rules of decltype
In this section, I will introduce the deduction rules of decltype. Basically, the type denoted by decltype(e) is defined as follows:
- If e is an unparenthesized id-expression or an unparenthesized class member access, 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.
Examples for rule 1:
int i = 4;
const int j = 6;
const int& k = i;
int&& m = 1;
int n[5];
int *p;
const bool f(int a);
int g(int a);
int g(char a);
struct A{
double x;
int y;
char f4(char a);
int& f5() const;
};
struct A aa;
struct A faa();
struct A* paa = &aa;
int main() {
decltype(i) var1; // int
decltype((short) i) var2; // short
decltype(j) var3 = 1; // const int
decltype(k) var4 = j; // const int&
decltype(m) var5 = 1; // int&&
decltype(n) var6; // int[5]
decltype(p) var7; // int*
decltype(A::y) var8; // int
decltype(&A::y) var9; // int A::*
decltype(&A::f4) var10; // char(A::*)(char)
decltype(&A::f5) var11; // int&(A::*)() const
decltype(aa.x) var12; // double
decltype(faa().x) var13; // double
decltype(paa->x) var14; // double
decltype(f) var15; // const bool(int)
decltype(g) var16; // error, g is a overload function
}
Rule 1 is the most validly used rule for decltype. It can be used to deduce the types of variables, pointers, arrays, and structures as the example shows.
Examples for Rule 2:
int x;
int&& f(){
return 1;
}
int main(){
decltype(static_cast<int&&>(x)) var1= 1; //int&&
decltype(f()) var2= 1; // int&&
}
Both static_cast<int&&>(x) and f() are xvalues. Xvalues refer to rvalue references that are going to expire.
Examples for Rule 3:
int i = 4;
int&& j = 1;
double k = 1.0;
int a[5];
int *p = &i;
struct A{
double x;
};
struct A aa;
struct A* paa = &aa;
int main() {
decltype(i=1) var1 = i; // int&, because assignment to int returns an lvalue
decltype(0, i) var2 = i; // int&
decltype(true ? i : i) var3 = i; // int&
decltype((i)) var4 = i; // int&
decltype(++i) var5 = i; // int&, ++i is an lvalue
decltype(a[3]) var6 = i; // int& ([] returns an lvalue)
decltype(*p) var7 = i; // int& (*operator returns an lvalue)
decltype((j)) var8 = i; // int&
decltype("decltype") var9 = "decltype"; // const char(&)[9] //string is the only one
// constant that is lvalue
decltype((aa.x)) var10 = k; // double&
decltype(0,aa.x) var11 = k; // double&
decltype((aa.*&A::x)) var12 = k; // double&
decltype((paa->x)) var13 = k; // double&
}
From the above example, you can see that decltype((i)) is deduced as int&, while decltype(i) is int. The deduced types are different because (i) is not an id-expression, so it does not meet Rule 1. At the same time, (i) is an lvalue, so it falls into the category of Rule 3. For the same reason, decltype((aa.x)), decltype((j)), and decltype((paa->x)) are all deduced as lvalue reference types. Decltype(0,i) is deduced as int& because variable i is of the int type, and “0,i” is not an id-expression.
Examples for Rule 4:
int i = 4;
template <class T> T f(const T& t){
return t;
}
struct A{
double x;
};
struct A faa();
const bool f1(int a);
void f2();
class C{
// f3 is of type void
decltype(f2()) f3() {
int i = 1; // C* (this is an rvalue)
}
};
int main(){
decltype(1) var1; // int
decltype(2+3) var2; // int (+ operator returns an rvalue)
decltype(i++) var3; // int (i++ is an rvalue)
decltype((faa().x)) var4 = i; // double (faa().x is an rvalue)
decltype(f(A())) var5; // A
decltype(f1(1)) var6 = 1; // const bool
decltype((f1(1))) var7 = 1; // const bool, parentheses can be ignored
decltype(&f1) var8; // const bool(*)(int)
decltype(*&f1) var9 = f1; // const bool(&)(int)
}
From this example, you can see Rule 4 is used for decltype deduction when e is a pointer to function, function call, non-string constant, or arithmetical expression.
Inheritance of cv-qualifiers and disposal of redundant symbols in decltype deductions
When you use decltype(expression) to get a type, and expression is an unparenthesized member variable of an object expression, the const or volatile qualifier of the parent object expression do not contribute to the result of decltype(expression). See the following example:
struct str{
int mem;
} x;
int main(){
const str y = {1};
volatile str * pz = &x;
decltype(y.mem) i; // int
decltype(pz->mem) j; // int
}
If expression declared in decltype(expression) is a parenthesized nonstatic non-reference class member variable, the const or volatile type qualifier of the parent object expression contributes to the result of decltype(expression). See the following example:
struct str{
int mem;
} x;
int main(){
const str y = {1};
volatile str * pz = &x;
int k;
decltype((y.mem)) i = k; // const int&
decltype((pz->mem)) j = k; // volatile int&
}
Redundant cv-qualifiers and operators in decltype deductions
The following redundant qualifiers or operators are ignored in decltype deductions:
- The const qualifier
- The volatile qualifier
- The & operator
See the following example:
int main(){
int i = 1;
int& j = i;
const int k = 1;
volatile int m = 1;
decltype(i)& var1 = i; //int&
decltype(j)& var2 = i; // int&, the redundant & operator is ignored
const decltype(k) var3 = 1; // const int, the redundant const qualifier is ignored
volatile decltype(m) var4 = 1; // volatile int, the redundant volatile qualifier is ignored
}
Please be aware that redundant * operators will not be ignored in decltype deductions. See the following example:
int main(){
int i = 1;
int * pj = &i;
decltype(pj) * var1 = &i; // error
decltype(pj) * var2 = &pj; // ok. The type of var2 is int **p
}