C++中typename关键字的使用方法和注意事项
起因
近日,看到这样一行代码:
typedef typename __type_traits<T>::has_trivial_destructor trivial_destructor;
看起来它应该是定义一个类型别名,但是typedef不应该是像这样使用么,typedef+原类型名+新类型名:
typedef char* PCHAR;
可为何此处多了一个typename?另外__type_traits又是什么?看起来有些眼熟,想起之前在Effective C++上曾经看过traits这一技术的介绍,和这里的__type_traits有点像。只是一直未曾遇到需要traits的时候,所以当时并未仔细研究。然而STL中大量的充斥着各种各样的traits,一查才发现原来它是一种非常高级的技术,在更现的高级语言中已经很普遍。因此这次花了些时间去学习它,接下来还有会有另一篇文章来详细介绍C++的traits技术。在这里,我们暂时忘记它,仅将它当成一个普通的类,先来探讨一下这个多出来的typename是怎么回事?
typename的常见用法
对于typename这个关键字,如果你熟悉C++的模板,一定会知道它有这样一种最常见的用法(代码摘自C++ Primer):
// implement strcmp-like generic compare function
// returns 0 if the values are equal, 1 if v1 is larger, -1 if v1 is smaller
template <typename T>
int compare(const T &v1, const T &v2)
{
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}
也许你会想到上面这段代码中的typename换成class也一样可以,不错!那么这里便有了疑问,这两种方式有区别么?查看C++ Primer之后,发现两者完全一样。那么为什么C++要同时支持这两种方式呢?既然class很早就已经有了,为什么还要引入typename这一关键字呢?问的好,这里面有一段鲜为人知的历史(也许只是我不知道:-))。带着这些疑问,我们开始探寻之旅。
typename的来源
对于一些更早接触C++的朋友,你可能知道,在C++标准还未统一时,很多旧的编译器只支持class,因为那时C++并没有typename关键字。记得我在学习C++时就曾在某本C++书籍上看过类似的注意事项,告诉我们如果使用typename时编译器报错的话,那么换成class即可。
template <class T>
int compare(const T &v1, const T &v2)
{
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}
从表面上看起来就好像这个模板的参数应该只支持用户自定义类型,所以使用语言内置类型或者指针来调用该模板函数时总会觉得有一丝奇怪(虽然并没有错误):
int v1 = 1, v2 = 2;
int ret = compare(v1, v2);
int *pv1 = NULL, *pv2 = NULL;
ret = compare(pv1, pv2);
令人感到奇怪的原因是,class在类和模板中表现的意义看起来存在一些不一致,前者针对用户自定义类型,而后者包含了语言内置类型和指针。故而觉得应该引入新的名称来特指。
一些关键概念
限定名和非限定名
限定名(qualified name),故名思义,是限定了命名空间的名称。看下面这段代码,cout和endl就是限定名:
#include <iostream>
int main() {
std::cout << "Hello world!" << std::endl;
}
out和endl前面都有std::,它限定了std这个命名空间,因此称其为限定名。
如果在上面这段代码中,前面用using std::cout;或者using namespace std;,然后使用时只用cout和endl,它们的前面不再有空间限定std::,所以此时的cout和endl就叫做非限定名(unqualified name)。
依赖名和非依赖名
依赖名(dependent name)是指依赖于模板参数的名称,而非依赖名(non-dependent name)则相反,指不依赖于模板参数的名称。看下面这段代码:
template <class T>
class MyClass {
int i;
vector<int> vi;
vector<int>::iterator vitr;
T t;
vector<T> vt;
vector<T>::iterator viter;
};
因为是内置类型,所以类中前三个定义的类型在声明这个模板类时就已知。然而对于接下来的三行定义,只有在模板实例化时才能知道它们的类型,因为它们都依赖于模板参数T。因此,T, vector和vector::iterator称为依赖名。前三个定义(i, vi, vitr) 叫做非依赖名。
更为复杂一点,如果用了typedef T U; U u;,虽然T没再出现,但是U仍然是依赖名。由此可见,不管是直接还是间接,只要依赖于模板参数,该名称就是依赖名。
类作用域
在类外部访问类中的名称时,可以使用类作用域操作符,形如MyClass::name的调用通常存在三种:静态数据成员、静态成员函数和嵌套类型:
struct MyClass {
static int A;
static int B();
typedef int C;
}
MyClass::A, MyClass::B, MyClass::C分别对应着上面三种。
引入typename的真实原因
一个例子
在Stroustrup起草了最初的模板规范之后,人们更加无忧无虑的使用了class很长一段时间。可是,随着标准化C++工作的到来,人们发现了模板这样一种定义:
template <class T>
void foo() {
T::iterator * iter;
// ...
}
这段代码的目的是什么?多数人第一反应可能是:作者想定义一个指针iter,它指向的类型是包含在类作用域T中的iterator。可能存在这样一个包含iterator类型的结构:
struct ContainsAType {
struct iterator { /*...*/ };
// ...
};
然后像这样实例化foo:
foo<ContainsAType>();
这样一来,iter那行代码就很明显了,它是一个ContainsAType::iterator类型的指针。到目前为止,咱们猜测的一点不错,一切都看起来很美好。
问题浮现
在类作用域一节中,我们介绍了三种名称,由于MyClass已经是一个完整的定义,因此编译期它的类型就可以确定下来,也就是说MyClass::A这些名称对于编译器来说也是已知的。
可是,如果是像T::iterator这样呢?T是模板中的类型参数,它只有等到模板实例化时才会知道是哪种类型,更不用说内部的iterator。通过前面类作用域一节的介绍,我们可以知道,T::iterator实际上可以是以下三种中的任何一种类型:
- 静态数据成员
- 静态成员函数
- 嵌套类型
前面例子中的ContainsAType::iterator是嵌套类型,完全没有问题。可如果是静态数据成员呢?如果实例化foo模板函数的类型是像这样的:
struct ContainsAnotherType {
static int iterator;
// ...
};
然后如此实例化foo的类型参数:
foo<ContainsAnotherType>();
那么,T::iterator * iter;被编译器实例化为ContainsAnotherType::iterator * iter;,这是什么?前面是一个静态成员变量而不是类型,那么这便成了一个乘法表达式,只不过iter在这里没有定义,编译器会报错:
error C2065: ‘iter’ : undeclared identifier
但如果iter是一个全局变量,那么这行代码将完全正确,它是表示计算两数相乘的表达式,返回值被抛弃。
同一行代码能以两种完全不同的方式解释,而且在模板实例化之前,完全没有办法来区分它们,这绝对是滋生各种bug的温床。这时C++标准委员会再也忍不住了,与其到实例化时才能知道到底选择哪种方式来解释以上代码,委员会决定引入一个新的关键字,这就是typename。
千呼万唤始出来
对于用于模板定义的依赖于模板参数的名称,只有在实例化的参数中存在这个类型名,或者这个名称前使用了typename关键字来修饰,编译器才会将该名称当成是类型。除了以上这两种情况,绝不会被当成是类型
因此,如果你想直接告诉编译器T::iterator是类型而不是变量,只需用typename修饰:
template <class T>
void foo() {
typename T::iterator * iter;
// ...
}
这样编译器就可以确定T::iterator是一个类型,而不再需要等到实例化时期才能确定,因此消除了前面提到的歧义。
使用typename的规则
-
typename在下面情况下禁止使用:
- 模板定义之外,即typename只能用于模板的定义中
- 非限定类型,比如前面介绍过的int,vector之类
- 基类列表中,比如template class C1 : T::InnerType不能在T::InnerType前面加typename
- 构造函数的初始化列表中
-
如果类型是依赖于模板参数的限定名,那么在它之前**必须加typename(**除非是基类列表,或者在类的初始化成员列表中)
-
其它情况下typename是可选的,也就是说对于一个不是依赖名的限定名,该名称是可选的,例如vector vi;
typedef typename __type_traits<T>::has_trivial_destructor trivial_destructor;
它是将__type_traits这个模板类中的has_trivial_destructor嵌套类型定义一个叫做trivial_destructor的别名,清晰明了