The decltype(expression) type specifier (C++11)

Note: IBM supports selected features of C++11, known as C++0x before its ratification. IBM will continue to develop and implement the features of this standard. The implementation of the language level is based on IBM's interpretation of the standard. Until IBM's implementation of all the C++11 features is complete, including the support of a new C++11 standard library, the implementation may change from release to release. IBM makes no attempt to maintain compatibility, in source, binary, or listings and other compiler interfaces, with earlier releases of IBM's implementation of the new C++11 features.

The decltype(expression) specifier is a type specifier introduced in C++11. With this type specifier, you can get a type that is based on the resultant type of a possibly type-dependent expression.

decltype(expression) takes expression as an operand. When you define a variable by using decltype(expression), it can be thought of as being replaced by the compiler with the type or the derived type of expression. Consider the following example:
int i;
static const decltype(i) j = 4;
In this example, decltype(i) is equivalent to the type name int.

General rules for using decltype

When you use decltype(expression) to get a type, the following rules are applicable:
  1. If expression is an unparenthesized id-expression or class member, decltype(expression) is the type of the entity named by expression. If there is no such entity, or if expression names a set of overloaded functions, the program is ill formed.
  2. Otherwise, if expression is an xvalue, decltype(expression) is T&&, where T is the type of expression.
  3. Otherwise, if expression is an lvalue, decltype(expression) is T&, where T is the type of expression.
  4. Otherwise, decltype(expression) is the type of expression.
The following example illustrates how these rules are used:
const int* g(){
    return new int[0];
}

int&& fun(){
   int&& var = 1;
   return 1;
}

struct A{
    double x;
};

template <class T> T tf(const T& t){
    return t;
}

bool f(){
    return false;
}

struct str1{
    template <typename T, typename U>
    static decltype((*(T*)0) * (*(U*)0)) mult(const U& arg1, const T& arg2){
       return arg1 * arg2;
    }
};

template <typename T, typename U>  struct str2{
    typedef decltype((*(T*)0) + (*(U*)0)) btype;
    static btype g(T t, U u);
};

int main(){
    int i = 4;
    const int j = 6;
    const int& k = i;
    int&& m = 1;
    int a[5];
    int *p;

    decltype(i) var1;            // int 
    decltype(1) var2;            // int 
    decltype(2+3) var3;          // int(+ operator returns an rvalue) 
    decltype(i=1) var4 = i;      // int&, because assignment to int 
                                 // returns an lvalue 
    decltype((i)) var5 = i;      // int& 
    decltype(j) var6 = 1;        // const int 
    decltype(k) var7 = j;        // const int& 
    decltype("decltype") var8 = "decltype";     // const char(&)[9]
    decltype(a) var9;            // int[5] 
    decltype(a[3]) var10 = i;    // int&([] returns an lvalue)
    decltype(*p)  var11 = i;     // int&(*operator returns an lvalue)
    decltype(fun()) var12 = 1;   // int&&
    decltype(tf(A())) var13;     // A 
    decltype(f()) var14;         // bool  
    decltype((f())) var15;       // bool, parentheses around f() are ignored
    decltype(f)  var16;          // bool() 
    decltype(&f) var17;          // bool(*)() 
    decltype(&A::x) var18;       // double A::* 
    decltype(str1::mult(3.0, 4u))  var19;           // double 
    decltype(str2<float, short>::g(1,3)) var20;     // float 
    decltype(m) var21 = 1;       // int&&
    decltype((m)) var22 = m;     // int&     
    return 0;
}
In this example, the comment after each decltype statement explains the type of the defined variable.
The following example illustrates an incorrect usage of decltype(expression):
int func(){
    return 0;
}
int func(int a){
    return 0;
}

int main(){
    int i = 4;
    
    // Incorrect usage. func names an overload function 
    decltype(func) var1;  
       
    // Correct usage. The overload operation is not ambiguous
    decltype(func(i)) var2;   
    
    return 0;
}
In this example, the compiler issues an error message because it does not know which func function to match.

Rules for using decltype with structure member variables

When you use decltype(expression) to get a type, and expression is an unparenthesized member variable of an object expression (with a . operator) or a pointer expression (with a -> operator), the following rules apply:
  • If the object expression or the pointer expression is specified with a constant or volatile qualifier, the type qualifier does not contribute to the result of decltype(expression).
  • The lvalueness or rvalueness of the object expression or the pointer expression does not affect whether decltype(expression) is a reference type or not.
Example:
struct Foo{
    int x;
};

int main(){
    struct Foo f;
    const struct Foo g = {0};
    volatile struct Foo* h = &f;
    struct Foo func();

    decltype(g.x) var1;         // int 
    decltype(h->x) var2;        // int 
    decltype(func().x) var3;    // int 
    return 0;
}
In this example, the constant qualifier of the object expression g is not desired in the result of decltype(g.x). Similarly, the volatile qualifier of the pointer expression h is not desired in the result of decltype(h->x). The object expression g and the pointer expression h are lvalues, and the object expression func() is an rvalue, but they do not affect whether the decltype results of their unparenthesized member variables are reference types or not.

If expression declared in decltype(expression) is a parenthesized nonstatic non-reference class member variable, the constant or volatile type qualifier of the parent object expression or pointer expression of expression contributes to the result of decltype(expression). Similarly, the lvalueness or rvalueness of the object expression or the pointer expression affects the result of decltype(expression).

Example:
struct Foo{
    int x;    
};

int main(){
    int i = 1;
    struct Foo f;
    const struct Foo g = {0};
    volatile struct Foo* h = &f;
    struct Foo func();

    decltype((g.x)) var1 = i;         // const int& 
    decltype((h->x)) var2 = i;        // volatile int& 
    decltype((func().x)) var3 = 1;    // int 
    return 0;
}
In this example, the result of decltype((g.x)) inherits the constant qualifier of the object expression g. Similarly, the result of decltype((h->x)) inherits the volatile qualifier of the pointer expression h. The object expression g and the pointer expression h are lvalues, so decltype((g.x)) and decltype((h->x)) are reference types. The object expression func() is an rvalue, so decltype((func().x)) is a nonreference type.

If you use the built-in operators .* or ->* within a decltype(expression), the constant or volatile type qualifier of the parent object expression or pointer expression of expression contributes to the result of decltype(expression), regardless of whether expression is a parenthesized or an unparenthesized structure member variable. Similarly, the lvalueness or rvalueness of the object expression or the pointer expression affects the result of decltype(expression).

Example:
class Foo{
    int x;
};
int main(){   
   int i = 0;
   Foo f;    
   const Foo & g = f;
   volatile Foo* h = &f;
   const Foo func();

   decltype(f.*&Foo::x)  var1 = i;        // int&, f is an lvalue
   decltype(g.*&Foo::x) var2 = i;         // const int&, g is an lvalue
   decltype(h->*&Foo::x) var3 = i;        // volatile int&, h is an lvalue
   decltype((h->*&Foo::x)) var4 = i;      // volatile int&, h is an lvalue
   decltype(func().*&Foo::x) var5 = 1;    // const int, func() is an rvalue
   decltype((func().*&Foo::x)) var6 = 1;  // const int, func() is an rvalue
   return 0;
}

Side effects and decltype

If you use decltype(expression) to get a type, additional operations in the decltype parenthetical context can be performed, but they do not have side effects outside of the decltype context. Consider the following example:
int i = 5;
static const decltype(i++) j = 4;    // i is still 5 
The variable i is not increased by 1 outside of the decltype context.
There are exceptions to this rule. In the following example, because the expression given to decltype must be valid, the compiler has to perform a template instantiation:
template <int N>
struct Foo{
    static const int n=N;
};
int i;

decltype(Foo<101>::n,i) var = i;     // int& 
In this example, Foo template instantiation occurs, even though var is only determined by the type of the variable i.

Redundant qualifiers and specifiers with decltype

Because decltype(expression) is considered syntactically to be a type specifier, the following redundant qualifiers or specifiers are ignored:
  • constant qualifiers
  • volatile qualifiers
  • & specifiers
The following example demonstrates this case:
int main(){
    int i = 5;
    int& j = i;
    const int k = 1;
    volatile int m = 1;

    // int&, the redundant & specifier is ignored
    decltype(j)&  var1 = i;      
    
    // const int, the redundant const qualifier is ignored 
    const decltype(k) var2 = 1;  

    // volatile int, the redundant volatile qualifer is ignored 
    volatile decltype(m) var3;   
    return 0;
}
Note: The functionality of ignoring the redundant & specifiers in decltype(expression) is not supported in the current C++11 standard, but it is implemented in this compiler release.

Template dependent names and decltype

Without using the decltype feature, when you pass parameters from one function to another function, you might not know the exact types of the results that are passed back. The decltype feature provides a mechanism to generalize the return types easily. The following program shows a generic function that performs the multiplication operation on some operands:
struct Math{
    template <typename T>
    static T mult(const T& arg1, const T& arg2){
        return arg1 * arg2;
    }
};
If arg1 and arg2 are not the same type, the compiler cannot deduce the return type from the arguments. You can use the decltype feature to solve this problem, as shown in the following example:
struct Foo{
   template<typename T, typename U>
   static decltype((*(T*)0)*(*(U*)0)) mult(const T& arg1, const U& arg2)
   {
       return arg1 * arg2;
   }
};
In this example, the return type of the function is the type of the multiplication result of the two template-dependent function parameters.

The typeof operator and decltype

IBM extension The decltype feature is similar to the existing typeof feature. One difference between these two features is that decltype accepts only an expression as its operand, while typeof can also accept a type name. Consider the following example:
__typeof__(int)  var1;     // okay 
decltype(int) var2;        // error 
In this example, int is a type name, so it is invalid as the operand of decltype.
Note: __typeof__ is an alternate spelling of typeof.