20180329 C++ 了解typename的双重意义
在一个template的声明式中,eg:
template<class T> class Widget; //使用"class"
template<typename T> class Widget; //使用"typename"
class和typename没有不同,即当我们声明template类型参数,class和typename的意义完全相同。有些人喜欢用class,因为可以少打几个字;有些人喜欢用typename,因为它暗示参数并非一定是class类型;有些人在接受任何类型时用typename,而在接受用户自定义类型时保留旧式的class。
在以下两种情况下,typename要特殊对待:
情况一:
假设有一个模板函数(template function)接受一个STL兼容容器为参数,容器内持有的对象可被赋值为ints,进一步假设这个函数仅仅打印其第二个函数(这个函数可能不会通过编译,先不管这个了),下面是一种实现方式:
template<typename C>
void print2nd(const C& container)//打印容器内的第二个元素
{ //注意:这不是有效的C++代码
if(container.size() >= 2)
{
C::const_iterator iter(container.begin());//取得第一个元素的迭代器
++iter; //将iter移往第二个元素
int value = *iter; //将该元素复制到某个int
std::cout<<value;//打印那个int
}
}
现在我们观察两个局部(local)变量iter和value:
iter的类型是C::const_iterator,它是什么,取决于模板(template)参数C。在模板中出现的名称若依赖于某个模板(template)参数,我们称之为从属名称(dependent names)。若从属名称在类内呈嵌套状,我们称它为嵌套从属名称(nested dependent name),C::const_iterator就是这样一个名称。实际上它还是个嵌套从属类型名称(nested dependent type name),也就是个嵌套从属名称并且指涉某个类型。
而print2nd里的另一个局部变量value,其类型为int,int是一个并不依赖任何模板参数的名称,我们管这样的名称叫非从属名称(non-dependent names)。
嵌套从属名称有可能会导致解析(parsing)困难。比如我们让print2nd的函数体执行以下操作:
template<typename C>
void print2nd(const C& container)
{
C::const_iterator* x;
...
}
看起来似乎我们要声明一个局部变量x,x是一个指针,指向一个C::const_iterator,但我们之所以这么任务,是因为声明已经知道了C::const_iterator是个类型,若C::const_iterator不是个类型呢?若C有个static成员变量而碰巧被命名为const_iterator呢?抑或如果x碰巧是个全局变量名称呢?那样的话上述代码就不再是声明为一个局部变量了,而是一个相乘操作:C::const_iterator 乘以 x。这听起来很疯狂,但这也是有可能的啊!
在我们知道C是什么之前,没有任何办法可以知道C::const_iterator是否为一个类型,而当编译器开始解析template print2nd时,尚未知道C是什么。C++有一个规则可以解析(resolve)这一歧义状态:若解析器在模板中遭遇一个嵌套从属名称,它便假设这个名称不是个类型,除非你告诉它是。所以缺省情况下嵌套从属名称不是类型,此规则有个例外,稍后再说。
在上例代码中,我们必须告诉C::const_iterator是个类型。只要紧邻它之前防止关键字typename即可:
template<typename C>
void print2nd(const C& container)
{
if(container.size() >= 2)
{
typename C::const_iterator iter(container.begin());
...
}
}
一般性规则很简单:任何时候要在模板中指涉一个嵌套从属类型名称时,就必须在紧邻它的前一个位置加上关键字typename(例外的情况,一会再说)。
typename只被用来验证嵌套从属类型名称;其他名称不该有他的存在。如下面这个函数模板,接受一个容器和一个“指向该容器”的迭代器:
template<typename C> //允许使用“typename”(或“class”)
void f(const C& container, //不允许使用“typename”
typename C::iterator iter); //一定要使用“typename”
上述的C并不是嵌套从事类型名称(它并非嵌套于任何“取决于模板参数”的东西里),所以声明container时并不需要以typename为前导,但C::iterator是个嵌套从属类型名称,所以必须以typename为前导。
“typename必须作为嵌套从属类型名称的前缀词”这一规则的例外就是:typename不可以出现在基类列表(base classes list)内的嵌套从属类型名称之前,也不可在成员初值列(member initialization list)中作为基类修饰符。eg:
template<typename T>
class Derived:public Base<T>::Nested //基类列表中不允许 typename
{
public:
explicit Derived(int x)
:Base<T>::Nested(x) //成员初值列中 不允许 typename
{
typename Base<T>::Nested temp;
/*
对于嵌套从属类型名称,当既不基类列表中也不在成员初值列中
作为一个基类修饰符需要加上 typename。
*/
...
}
...
};
再看另外一个例子:
//撰写一个函数模板,它接受一个迭代器,为该迭代器指涉的对象做一份局部复件temp
template<typename IterT>
void workWithIterator(IterT iter)
{
typename std::iterator_traits<IterT>::value_type temp(*iter);
...
}
std::iterator_traits<IterT>::value_type的意思是:类型为IterT的对象所指之物的类型,这个语句声明一个局部变量temp,使用IterT对象所指物的相同类型,将temp初始化为iter所指物。若IterT是vector<int>::iterator,temp的类型就是int。
由于std::iterator_traits<IterT>::value_type是个嵌套从属类型名称(value_type被嵌套于iterator_traits<IterT>里而IterT是个模板参数),所以必须在他之前放置typename。
如果你认为std::iterator_traits<IterT>::value_type读起来不顺畅,也可以这样写:
void workWithIterator(IterT iter)
{
typedef typename std::iterator_traits<IterT>::value_type;
value_type temp(*iter);
...
}
这就看你的使用习惯了。
注意:
1)声明模板参数时,前缀关键字class和typename可以互换;
2)请使用关键字typename标识嵌套从属类型名称;但不可以在基类列(base class lists)或成员初值列(member initialization list)里以它作为基类修饰符。