博客:https://goodfanqie.github.io(文章优先展示地方,推荐在此阅读。)
typename的疑问
最近在学习《STL 源码剖析》这本书的时候,在里面看到其中关于typename的用法令我非常的疑惑。
typedef typename _type_traits<T>::has_trivial_destory trivial_destructor;
看到这行代码的时候我疑惑得去数了一下这中间的空格数,因为在印象中,typedef的用法是原类型名加新类型名,但是这里多出来的typename让我觉得很疑惑,后面查阅了很多资料翻了很多博客才慢慢想清楚了这其中的原因。我们先撇开后面跟的那一大串type_traits不谈,因为这个需要单独一篇博客才讲得清楚。我们先从简单的例子开始。
typename的最常用法
typename通常用于在模版的定义中,如下:
template <typename T> class Test;
template <class T> class Test2;
在上述定义中,对于C++本身来说,typename和class是完全没有区别的。但是对于C++来说这样有什么好处吗,为什么要造两个意义一样的关键字,我们从他们的起源开始。
对于一些更早接触C++的朋友,你可能知道,在C++标准还未统一时,很多旧的编译器只支持class,因为那时C++并没有typename关键字。记得我在学习C++时就曾在某本C++书籍上看过类似的注意事项,告诉我们如果使用typename时编译器报错的话,那么换成class即可。
一切归结于历史。
Stroustrup在最初起草模板规范时,他曾考虑到为模板的类型参数引入一个新的关键字,但是这样做很可能会破坏已经写好的很多程序(因为class已经使用了很长一段时间)。但是更重要的原因是,在当时看来,class已完全足够胜任模板的这一需求,因此,为了避免引起不必要的麻烦,他选择了妥协,重用已有的class关键字。所以只到ISO C++标准出来之前,想要指定模板的类型参数只有一种方法,那便是使用class。这也解释了为什么很多旧的编译器只支持class。
class在模版中遇到的麻烦
我们谈到,Stroustrup在准备引入新关键字的时候,class在当时似乎已经完全胜任了模版对象的这一个需求,但是在越来越多的实践过程中,class的缺点或者说template的缺点出现了。
假设你现在要针对某一种容器设定一个操作函数:
template <class T>
void func (){
T::iteartor * testpt;
}
看到这段代码的时候我们大多数情况下都是可以看出来,这一段代码中的操作是定义了一个容器的迭代器指针类型的变量。但是模版是在编译期间展开的,只有在模版实例化的时候编译器才可以推导出其类型。这段代码对于编译器来说很有可能产生错误的理解,因为我们能快速的根据iteartor是一个迭代器想到这是定义了一个变量,但是对于编译器来说,它怎么会知道一定知道T::iteartor一定是一个迭代器类型,或者一定知道这是一个类型?因为能表示成这样形式的代码有三种情况:
- 在T作用域中存在一个iteartor的静态变量
- 在T作用域中存在一个iteartor的静态成员函数
- 是T类型的成员变量
以上三种含义均可以表示成例子中的样子,编译器怎么知道这是哪一种。在实践过程中,编译器会直接对testpt报错:
error: use of undeclared identifier 'testpt'
typename的真正用途
编译期间模版的推导有一个这样的规则:如果解析器在template推导期间遇到了嵌套从属名称,那么不指定他为一个类型,解析器就一定不会把它当成一个类型。什么是从属类型,就是形如T::iteartor
这种,这也就是为什么编译器会对testpt报错的原因。那要怎样指定testpt为一个类型,这就回到了开头的那个问题,我们可以这样解决:
template <class T>
void func (){
typename T::iteartor * testpt;
}
加上了typename之后我们就可以知道T::iteartor是一个类型,编译器也可以根据这个进行类型推导了。