在 《Effective C++》条款42:了解typename的双重意义 中,对此进行了介绍,首先明确 typename 关键字只在 c++ 的模板和泛型编程中才会出现。
在学习STL的过程当中,经常会遇到一些让人难以理解的C++代码,比如:
typedef typename std::vector<T>::size_type size_type;
看起来它应该是定义一个类型别名,但是typedef
使用方式应该是typedef + 原类型名 + 新类型名
(typedef具体使用方法可移步 typedef和#define的用法与区别 ),为何此处多了个typename
?typename又是什么东西?
typedef char* PCHAR;
只记得泛型编程(GP)当中,若是对模板类/函数进行定义时,可以使用template<class T>
或者template<typename T>
,且这两者是等价的。
那么如上语句中的typename关键字是起了什么作用呢?
1. std::vector<T>::size_type 是什么?
查看侯捷的《STL源码剖析》一书,其为:
template <class T,class Alloc=alloc>
class vector{
public:
//...
typedef size_t size_type;
//...
};
原来vector::size_type
是vector
中的嵌套类型,其实际等价于STL中大面积使用的可跨平台的size_t
类型。
2. 使用 typename 关键字的意义
将语句中的typename
关键字抛开不看,则语句为:
typedef std::vector<T>::size_type size_type;
可以感觉到是对std::vector<T>::size_type
这个类型进行重命名。那么为什么要加上typename
这个关键字呢?
原来,其中T
是模板中的类型参数,它只有等到模板实例化时才会知道是哪种类型,更不用说T
内部的size_type
。所以对于限定依赖名(解释见补充)std::vector<T>::size_type
而言,无法判断其是静态成员变量/函数还是嵌套类型,这样会造成语句的二义性,这是不能够容忍的。
若是在std::vector<T>::size_type
这个类型前加上typename
这一关键字,指明这是一个嵌套类型,而不是T
的静态成员或静态成员函数,消除了二义性。
3. 总结
根据以上的分析,可以知道语句的含义和作用是:
typedef
创建了存在类型的别名,而typename
告诉编译器std::vector<T>::size_type
是一个类型而不是一个成员。
拓展:为何会有typename关键字
若是不加 typename
时,在如下模板定义会造成语句的二义性:
template <class T>
void foo() {
T::iterator * iter;
// ...
}
struct ContainsAnotherType {
static int iterator;
// ...
};
然后如此实例化foo
的类型参数:
foo<ContainsAnotherType>();
那么,T::iterator * iter;
被编译器实例化为ContainsAnotherType::iterator * iter;
。在这里,前面是一静态成员变量而不是类型,那么这便成了一个乘法表达式,不过iter
在这里没有定义,编译器会报错:
error C2065: ‘iter’ : undeclared identifier
但如果iter
是一个全局变量,那么这行代码将完全正确,它是表示计算两数相乘的表达式,返回值被抛弃。
如上造成了同一行代码能以两种完全不同的方式解释,而且在模板实例化之前,完全没有办法来区分它们。
为解决该问题,C++标准委员会引入了typename
关键字,使得模板类/函数在实例化前就区分其内部的依赖名(例如:vector<T>::iterator viter;
, 也称dependent names)为静态成员变量/函数还是嵌套类型。
补充:类外部访问类内成员或名称、(非)限定名、(非)依赖名
一、 类外部访问类内成员或名称
在类作用域概念中,在类外部访问类中的名称时,可以使用类作用域操作符::
,形如MyClass::name
的调用通常存在三种:①静态数据成员、②静态成员函数和③嵌套类型:
struct MyClass {
static int A;
static int B();
typedef int C;
}
MyClass::A
, MyClass::B
, MyClass::C
分别对应着上面三种。
二、限定名和非限定名
限定名(qualified name),就是限定了命名空间的名称。看下面这段代码,cout
和endl
就是限定名:
#include <iostream>
int main() {
std::cout << "Hello world!" << std::endl;
}
cout
和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<T>
和vector<T>::iterator
称为依赖名。前三个定义叫做非依赖名。
更为复杂一点,如果用了typedef T U; U u;
,虽然T
没再出现,但是U
仍然是依赖名。由此可见,不管是直接还是间接,只要依赖于模板参数,该名称就是依赖名。
参考资料
-
《STL源码剖析》
-
《C++ Primer(第6版)》
-
Where and why do I have to put the “template” and “typename” keywords? 中的 answer #1225