首先大家看下面一段代码:
1
2
3
4
5
6
7
8
9
10
template<typename T>
std::vector<T>& arrayRotate(const std::vector<T>& src)
{
std::vector<T> dst(src.size());
for (std::vector<T>::iterator iter = src.begin(); src.end() != iter; iter++)
{
dst.push_back(*iter);
}
return dst;
}
如果能一眼发现哪里写错了,那么可以不用往下看了(手动狗头)
如果在VC中编译会提示for (std::vector<T>::iterator iter = src.begin(); src.end() != iter; iter++)这一行少了个;,但这明显不可能,仔细看下行中每个token的Code IntelliSense会发现在iter这个词上会提示<error-type> iter,那么问题很明显了,std::vector<T>::iterator这个类型无法别编译器确定。解决方案就很简单了,直接在前面加上typename以表明这是一个类型名字:
1
for (typename std::vector<T>::const_iterator iter = src.begin(); src.end() != iter; iter++)
再次编译,问题解决。(《C++ Primer》提到这里不能用class来代替typename[3],但我在VC2019中测试过这里不用typename而使用class也是可以的,可能是微软为了兼容早期C++中并没有typename的情况,在其他编译器中不一定可以通过,因此你也应当尽量只使用typename)
那么接下来我们就来探讨一下这个C++ Template中比较容易被忽略的问题之一:typename关键字。
1 关键字typename
看下面这个例子:
1
2
3
4
template <typename T>
class SomeClass {
typename T::SubType * ptr;
}
这里和本文开头有问题程序中的std::vector<T>::iterator要使用typename的目的是一样的,typename是为了说明模板内部的标识符可以是一个类型。SubType是定义在类T内部的一种类型。因此,ptr是一个指向T::SubType类型的指针。如果不使用typename,那么SubType就会被认为是一个静态成员,那么它就会被解析成具体的变量或者对象,于是T::SubType * ptr就会被看作是类T的静态成员SubType和ptr的乘积[1]。
2 .template和->template
看下面这个例子:
1
2
3
4
5
template <int N>
void printBitset (std::bitset<N> const& bs)
{
std::cout << bs.template to_string<char, char_traits<char>, allocator<char> >();
}
其实这段代码本意是想调用bs的to_string成员函数,这个成员函数原型是这样的(注:在C++ 11以前,这个函数叫做basic_string,于C++ 11改名为to_string):
1
2
3
4
5
6
template<
class CharT = char,
class Traits = std::char_traits<CharT>,
class Allocator = std::allocator<CharT>
> std::basic_string<CharT,Traits,Allocator>
to_string(CharT zero = CharT('0'), CharT one = CharT('1')) const;
那么这里为何不直接调用bs.to_string而要插入一个奇怪的.template声明呢?原因在于这里传入参数bs是依赖于模板参数N构造的,如果没有这个声明,编译器无法确定后面的小于号(<)并不是数学中的小于号,而是模板是参列表的起始符号[1]。
因此,我们不难总结出,只有当前面存在依赖于模板参数的对象时,才需要在模板内部使用.template或者->template来避免产生二义性
3 产生二义性的原因
其实前文已经提到,产生二义性的原因主要是因为这些地方都是跟模板参数有关的。在引入模板以后,我们可以把C++中的name分为两种:Dependent names和Non-dependent names[2]。所谓Non-dependent names顾名思义就是完全独立的名字,而Dependent names就是受制于模板参数影响的名字,在模板具象化之前是是无法真正确定这个名字含义的。(有的地方可能会使用实例化这个词,但我更偏好于具象化,以区别于类的实例化)因此就需要一些额外的语法来保证编译器正确工作。
4 其他语言的处理方式
这套规则查看下总感觉没那么智能,而且有传言本来C++ 11是要优化这个问题的[2],但似乎现在其实没有太大改变。那么我们来看看同样具有template语法的Java是如何处理这个问题的吧。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Pair<T>
{
private T value;
private SubType subObject;
public Pair(T val) {
value = val;
subObject = new SubType();
subObject.subValue = 1000;
}
public class SubType {
public int subValue;
}
public SubType getSubObject() {
return subObject;
}
}
public class Main {
public static void main(String[] args) {
Pair<Integer> pair = new Pair<>(666);
Pair<Integer>.SubType subObject = pair.getSubObject();
int result = subObject.subValue;
System.out.println(result);
}
}
这里并没有类似C++中的typename这样的关键字,编译,运行,成功获得结果:
1
1000
看来Java中并没有C++那么多的约束,但这实际上是因为Java中没有模板特化(Template Specialize)这种东西,呃,这又是一个很大的话题了,以后有机会再说吧(笑)。
画外音:这次标题怎么又是这个句式Orz
参考文献: [1] 《C++ Templates》, [美]David Vandevoorde [德]Nicolai M. Josuttis, 陈伟柱 译, 人民邮电出版社, 2013 [2] 《A Description of the C++ typename keyword》, Evan Driscoll, http://pages.cs.wisc.edu/~driscoll/typename.html [3] 《C++ Primer 5e》, Stanley B. Lippman, 《电子工业出版社》, 2013.9