IBM Z and LinuxONE - Languages - Group home

C++11: The decltype specifier

  

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:

  1. 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.
  2. Otherwise, if e is an xvalue, decltype(e) is T&&, where T is the type of e.
  3. Otherwise, if e is an lvalue, decltype(e) is T&, where T is the type of e.
  4. 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

}