1.?获知表达式的类型
上一节所讲的auto,用于通过一个表达式在编译时确定待定义的变量类型,auto所修饰的变量必须被初始化,编译器需要通过初始化来确定auto所代表的类型,即必须要定义变量。若仅希望得到类型,而不需要(或不能)定义变量的时候应该怎么办呢?
C++11新增了decltype关键字,用来在编译时推导出一个表达式的类型。它的语法格式如下:
decltype(exp)
其中,exp表示一个表达式(expression)。
从格式上来看,decltype很像sizeof——用来推导表达式类型大小的操作符。类似于sizeof,decltype的推导过程是在编译期完成的,并且不会真正计算表达式的值。
那么怎样使用decltype来得到表达式的类型呢?让我们来看一组例子:
int x = 0; decltype(x) y = 1; // y -> int decltype(x + y) z = 0; // z -> int const int& i = x; decltype(i) j = y; // j -> const int & const decltype(z) * p = &z; // *p -> const int, p -> const int * decltype(z) * pi = &z; // *pi -> int , pi -> int * decltype(pi)* pp = π // *pp -> int * , pp -> int * *
y和z的结果表明decltype可以根据表达式直接推导出它的类型本身。这个功能和上一节的auto很像,但又有所不同。auto只能根据变量的初始化表达式推导出变量应该具有的类型。若想要通过某个表达式得到类型,但不希望新变量和这个表达式具有同样的值,此时auto就显得不适用了。
j的结果表明decltype通过表达式得到的类型,可以保留住表达式的引用及const限定符。实际上,对于一般的标记符表达式(id-expression),decltype将精确地推导出表达式定义本身的类型,不会像auto那样在某些情况下舍弃掉引用和cv限定符。
p、pi的结果表明decltype可以像auto一样,加上引用和指针,以及cv限定符。
pp的推导则表明,当表达式是一个指针的时候,decltype仍然推导出表达式的实际类型(指针类型),之后结合pp定义时的指针标记,得到的pp是一个二维指针类型。这也是和auto推导不同的一点。
对于decltype和引用(&)结合的推导结果,与C++11中新增的引用折叠规则(Reference Collapsing)有关,因此,留到后面的2.1节右值引用(Rvalue Reference)时再详细讲解。
关于p、pi、pp的推导,有个很有意思的地方。像MicrosoftVisual Studio这样的IDE,可以在运行时观察每个变量的类型。我们可以看到p的显示是这样的:
这其实是C/C++的一个违反常理的地方:指针(*)、引用(&)属于说明符(declarators),在定义的时候,是和变量名,而不是类型标识符(type-specif?iers)相结合的。
因此,“const decltype(z)*p”推导出来的其实是*p的类型(const int),然后再进一步运算出p的类型。
2.decltype的推导规则
从上面一节内容来看,decltype的使用是比较简单的。但在简单的使用方法之后,也隐藏了不少细节。
我们先来看看decltype(exp)的推导规则:
推导规则1,exp是标识符、类访问表达式,decltype(exp)和exp的类型一致。
推导规则2,exp是函数调用,decltype(exp)和返回值的类型一致。
推导规则3,其他情况,若exp是一个左值,则decltype(exp)是exp类型的左值引用,否则和exp类型一致。
只看上面的推导规则,很难理解decltype(exp)到底是一个什么类型。为了更好地讲解这些规则的适用场景,下面根据上面的规则分3种情况依次讨论:
1)标识符表达式和类访问表达式。
2)函数调用(非标识符表达式,也非类访问表达式)。
3)带括号的表达式和加法运算表达式(其他情况)。
(1)标识符表达式和类访问表达式
先看第一种情况,代码清单1-3是一组简单的例子。
代码清单1-3 decltype作用于标识符和类访问表达式示例
class Foo { public: static const int Number = 0; int x; }; int n = 0; volatile const int & x = n; decltype(n) a = n; // a -> int decltype(x) b = n; // b -> const volatile int & decltype(Foo::Number) c = 0; // c -> const int Foo foo; decltype(foo.x) d = 0; // d -> int,类访问表达式
变量a、b、c保留了表达式的所有属性(cv、引用)。这里的结果是很简单的,按照推导规则1,对于标识符表达式而言,decltype的推导结果就和这个变量的类型定义一致。
d是一个类访问表达式,因此也符合推导规则1。
(2)函数调用
接下来,考虑第二种情况:如果表达式是一个函数调用(不符合推导规则1),结果会如何呢?
请看代码清单1-4所示的示例。
代码清单1-4 decltype作用于函数调用的示例
int& func_int_r(void); // 左值(lvalue,可简单理解为可寻址值) int&& func_int_rr(void); // x值(xvalue,右值引用本身是一个xvalue) int func_int(void); // 纯右值(prvalue,将在后面的章节中讲解) const int& func_cint_r(void); // 左值 const int&& func_cint_rr(void); // x值 const int func_cint(void); // 纯右值 const Foo func_cfoo(void); // 纯右值 // 下面是测试语句 int x = 0; decltype(func_int_r()) a1 = x; // a1 -> int & decltype(func_int_rr()) b1 = 0; // b1 -> int && decltype(func_int()) c1 = 0; // c1 -> int decltype(func_cint_r()) a2 = x; // a2 -> const int & decltype(func_cint_rr()) b2 = 0; // b2 -> const int && decltype(func_cint()) c2 = 0; // c2 -> int decltype(func_cfoo()) ff = Foo(); // ff -> const Foo
可以看到,按照推导规则2,decltype的结果和函数的返回值类型保持一致。
这里需要注意的是,c2是int而不是const int。这是因为函数返回的int是一个纯右值(prvalue)。对于纯右值而言,只有类类型可以携带cv限定符,此外则一般忽略掉cv限定。
如果在gcc下编译上面的代码,会得到一个警告信息如下:
warning: type qualif?iers ignored on function return type [-Wignored-qualif?iers] cint func_cint(void);
因此,decltype推导出来的c2是一个int。
作为对比,可以看到decltype根据func_cfoo()推导出来的ff的类型是const Foo。
(3)带括号的表达式和加法运算表达式
最后,来看看第三种情况:
struct Foo { int x; }; const Foo foo = Foo(); decltype(foo.x) a = 0; // a -> int decltype((foo.x)) b = a; // b -> const int & int n = 0, m = 0; decltype(n + m) c = 0; // c -> int decltype(n += m) d = c; // d -> int &
a和b的结果:仅仅多加了一对括号,它们得到的类型却是不同的。
a的结果是很直接的,根据推导规则1,a的类型就是foo.x的定义类型。
b的结果并不适用于推导规则1和2。根据foo.x是一个左值,可知括号表达式也是一个左值。因此可以按照推导规则3,知道decltype的结果将是一个左值引用。
foo的定义是const Foo,所以foo.x是一个const int类型左值,因此decltype的推导结果是const int &。
同样,n+m返回一个右值,按照推导规则3,decltype的结果为int。
最后,n+=m返回一个左值,按照推导规则3,decltype的结果为int &。
3.?decltype的实际应用
decltype的应用多出现在泛型编程中。考虑代码清单1-5的场景。
代码清单1-5 泛型类型定义可能存在问题的示例
#include <vector> template <class ContainerT> class Foo { typename ContainerT::iterator it_; // 类型定义可能有问题 public: void func(ContainerT& container) { it_ = container.begin(); } // ... }; int main(void) { typedef const std::vector<int> container_t; container_t arr; Foo<container_t> foo; foo.func(arr); return 0; }
单独看类Foo中的it_成员定义,很难看出会有什么错误,但在使用时,若上下文要求传入一个const容器类型,编译器马上会弹出一大堆错误信息。
原因就在于,ContainerT::iterator并不能包括所有的迭代器类型,当ContainerT是一个const类型时,应当使用const_iterator。
要想解决这个问题,在C++98/03下只能想办法把const类型的容器用模板特化单独处理,比如增加一个像下面这样的模板特化:
template <class ContainerT> class Foo<const ContainerT> { typename ContainerT::const_iterator it_; public: void func(const ContainerT& container) { it_ = container.begin(); } // ... };
这实在不能说是一个好的解决办法。若const类型的特化只是为了配合迭代器的类型限制,Foo的其他代码也不得不重新写一次。
有了decltype以后,就可以直接这样写:
template <class ContainerT> class Foo { decltype(ContainerT().begin()) it_; public: void func(ContainerT& container) { it_ = container.begin(); } // ... };
是不是舒服很多了?
decltype也经常用在通过变量表达式抽取变量类型上,如下面的这种用法:
vector<int> v; // ... decltype(v)::value_type i = 0;
在冗长的代码中,人们往往只会关心变量本身,而并不关心它的具体类型。比如在上例中,只要知道v是一个容器就够了(可以提取value_type),后面的所有算法内容只需要出现v,而不需要出现像vector<int>这种精确的类型名称。这对理解一些变量类型复杂但操作统一的代码片段有很大好处。
实际上,标准库中有些类型都是通过decltype来定义的:
typedef decltype(nullptr)nullptr_t;// 通过编译器关键字nullptr定义类型nullptr_t
typedef decltype(sizeof(0)) size_t;
这种定义方法的好处是,从类型的定义过程上就可以看出来这个类型的含义。