C++ decltype(类型推导)精讲
学习了《C++ auto》一节我们应该知道,auto 用于通过一个表达式在编译时确定待定义的变量类型,auto 所修饰的变量必须被初始化,编译器需要通过初始化来确定 auto 所代表的类型,即必须要定义变量。若仅希望得到类型,而不需要(或不能)定义变量的时候应该怎么办呢?
C++11 新增了 decltype 关键字,用来在编译时推导出一个表达式的类型。它的语法格式如下:
从格式上来看,decltype 很像 sizeof ——用来推导表达式类型大小的操作符。类似于 sizeof,decltype 的推导过程是在编译期完成的,并且不会真正计算表达式的值。
那么怎样使用 decltype 来得到表达式的类型呢?让我们来看一组例子:
1) y 和 z 的结果表明 decltype 可以根据表达式直接推导出它的类型本身。这个功能和上一节的 auto 很像,但又有所不同。auto 只能根据变量的初始化表达式推导出变量应该具有的类型。若想要通过某个表达式得到类型,但不希望新变量和这个表达式具有同样的值,此时 auto 就显得不适用了。
2) j 的结果表明 decltype 通过表达式得到的类型,可以保留住表达式的引用及 const 限定符。实际上,对于一般的标记符表达式(id-expression),decltype 将精确地推导出表达式定义本身的类型,不会像 auto 那样在某些情况下舍弃掉引用和 cv 限定符。
3) p、pi 的结果表明 decltype 可以像 auto 一样,加上引用和指针,以及 cv 限定符。
4) pp 的推导则表明,当表达式是一个指针的时候,decltype 仍然推导出表达式的实际类型(指针类型),之后结合 pp 定义时的指针标记,得到的 pp 是一个二维指针类型。这也是和 auto 推导不同的一点。
对于 decltype 和引用(&)结合的推导结果,与 C++11 中新增的引用折叠规则(Reference Collapsing)有关,因此,留到后面右值引用(Rvalue Reference)一节再详细讲解。
这其实是 C/C++ 的一个违反常理的地方:指针(*)、引用(&)属于说明符(declarators),在定义的时候,是和变量名,而不是类型标识符(type-specifiers)相结合的。
因此,
我们先来看看 decltype(exp) 的推导规则:
【实例】decltype 作用于标识符和类访问表达式示例。
d 是一个类访问表达式,因此也符合推导规则1。
【实例】decltype 作用于函数调用的示例。
这里需要注意的是,c2 是 int 而不是 const int。这是因为函数返回的 int 是一个纯右值(prvalue)。对于纯右值而言,只有类类型可以携带 cv 限定符,此外则一般忽略掉 cv 限定。
如果在 gcc 下编译上面的代码,会得到一个警告信息如下:
作为对比,可以看到 decltype 根据 func_cfoo() 推导出来的 ff 的类型是 const Foo。
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&。
【实例】泛型类型定义可能存在问题的示例。
原因就在于,ContainerT::iterator 并不能包括所有的迭代器类型,当 ContainerT 是一个 const 类型时,应当使用 const_iterator。
要想解决这个问题,在 C++98/03 下只能想办法把 const 类型的容器用模板特化单独处理,比如增加一个像下面这样的模板特化:
有了 decltype 以后,就可以直接这样写:
decltype 也经常用在通过变量表达式抽取变量类型上,如下面的这种用法:
实际上,标准库中有些类型都是通过 decltype 来定义的:
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 * *对代码的说明:
1) y 和 z 的结果表明 decltype 可以根据表达式直接推导出它的类型本身。这个功能和上一节的 auto 很像,但又有所不同。auto 只能根据变量的初始化表达式推导出变量应该具有的类型。若想要通过某个表达式得到类型,但不希望新变量和这个表达式具有同样的值,此时 auto 就显得不适用了。
2) j 的结果表明 decltype 通过表达式得到的类型,可以保留住表达式的引用及 const 限定符。实际上,对于一般的标记符表达式(id-expression),decltype 将精确地推导出表达式定义本身的类型,不会像 auto 那样在某些情况下舍弃掉引用和 cv 限定符。
3) p、pi 的结果表明 decltype 可以像 auto 一样,加上引用和指针,以及 cv 限定符。
4) pp 的推导则表明,当表达式是一个指针的时候,decltype 仍然推导出表达式的实际类型(指针类型),之后结合 pp 定义时的指针标记,得到的 pp 是一个二维指针类型。这也是和 auto 推导不同的一点。
对于 decltype 和引用(&)结合的推导结果,与 C++11 中新增的引用折叠规则(Reference Collapsing)有关,因此,留到后面右值引用(Rvalue Reference)一节再详细讲解。
扩展阅读
关于 p、pi、pp 的推导,有个很有意思的地方。像 Microsoft Visual Studio 这样的 IDE,可以在运行时观察每个变量的类型。我们可以看到 p 的显示是这样的:这其实是 C/C++ 的一个违反常理的地方:指针(*)、引用(&)属于说明符(declarators),在定义的时候,是和变量名,而不是类型标识符(type-specifiers)相结合的。
因此,
const decltype(z)*p
推导出来的其实是 *p 的类型(const int),然后再进一步运算出 p 的类型。
decltype 的推导规则
从前面的内容来看,decltype 的使用是比较简单的。但在简单的使用方法之后,也隐藏了不少细节。我们先来看看 decltype(exp) 的推导规则:
- 推导规则1,exp 是标识符、类访问表达式,decltype(exp) 和 exp 的类型一致。
- 推导规则2,exp 是函数调用,decltype(exp) 和返回值的类型一致。
- 推导规则3,其他情况,若 exp 是一个左值,则 decltype(exp) 是 exp 类型的左值引用,否则和 exp 类型一致。
关于推导规则,有很多种版本:只看上面的推导规则,很难理解decltype(exp) 到底是一个什么类型。为了更好地讲解这些规则的适用场景,下面根据上面的规则分3种情况依次讨论:
C++ 标准:ISO/IEC 14882:2011,7.1.6.2 Simple type specif iers,第4 款
MSDN:decltype Type Specif ier,http://msdn.microsoft.com/en-us/library/dd537655.aspx
维基百科:decltype,http://en.wikipedia.org/wiki/Decltype
虽然描述不同,但其实是等价的。为了方便理解,这里选取了 MSDN 的版本。
- 标识符表达式和类访问表达式。
- 函数调用(非标识符表达式,也非类访问表达式)。
- 带括号的表达式和加法运算表达式(其他情况)。
1) 标识符表达式和类访问表达式
先看第一种情况,下面的代码是一组简单的例子。【实例】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),结果会如何呢?请看下面的代码。【实例】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-qualifiers] 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&。
decltype 的实际应用
decltype 的应用多出现在泛型编程中,请看下面的例子。【实例】泛型类型定义可能存在问题的示例。
#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;这种定义方法的好处是,从类型的定义过程上就可以看出来这个类型的含义。
所有教程
- C语言入门
- C语言编译器
- C语言项目案例
- 数据结构
- C++
- STL
- C++11
- socket
- GCC
- GDB
- Makefile
- OpenCV
- Qt教程
- Unity 3D
- UE4
- 游戏引擎
- Python
- Python并发编程
- TensorFlow
- Django
- NumPy
- Linux
- Shell
- Java教程
- 设计模式
- Java Swing
- Servlet
- JSP教程
- Struts2
- Maven
- Spring
- Spring MVC
- Spring Boot
- Spring Cloud
- Hibernate
- Mybatis
- MySQL教程
- MySQL函数
- NoSQL
- Redis
- MongoDB
- HBase
- Go语言
- C#
- MATLAB
- JavaScript
- Bootstrap
- HTML
- CSS教程
- PHP
- 汇编语言
- TCP/IP
- vi命令
- Android教程
- 区块链
- Docker
- 大数据
- 云计算