从typename std::vector<T>::iterator说起

2,354 阅读4分钟

首先大家看下面一段代码:

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;
}

如果能一眼发现哪里写错了,那么可以不用往下看了,你已经是半个C++ template专家了(笑)

如果在VC中编译会提示for (std::vector<T>::iterator iter = src.begin(); src.end() != iter; iter++)这一行少了个;,但这明显不可能,仔细看下行中每个tokenCode IntelliSense会发现在iter这个词上会提示<error-type> iter,那么问题很明显了,std::vector<T>::iterator这个类型无法别编译器确定。解决方案就很简单了,直接在前面加上typename以表明这是一个类型名字

for (typename std::vector<T>::const_iterator iter = src.begin(); src.end() != iter; iter++)

再次编译,问题解决。(《C++ Primer》提到这里不能用class来代替typename[3],但我在VC2019中测试过这里不用typename而使用class也是可以的,可能是微软为了兼容早期C++中并没有typename的情况,在其他编译器中不一定可以通过,因此你也应当遵守Stanley提到的只使用typename

那么接下来我们就来探讨一下这个C++ Template中比较容易被忽略的问题之一:typename关键字。

1 关键字typename

看下面这个例子:

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的静态成员SubTypeptr的乘积[1]。

2 .template->template

看下面这个例子:

template <int N>
void printBitset (std::bitset<N> const& bs)
{
    std::cout << bs.template to_string<char, char_traits<char>, allocator<char> >();
}

其实这段代码本意是想调用bsto_string成员函数,这个成员函数原型是这样的(注意这里用的是C++ 11以后的函数原型,之前的函数原型叫做basic_string):

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 namesNon-dependent names[2]。所谓Non-dependent names顾名思义就是完全独立的名字,而Dependent names就是受制于模板参数影响的名字,在模板具象化之前是是无法真正确定这个名字含义的。(有的地方可能会使用实例化这个词,但我更偏好于具象化,以区别于类的实例化)因此就需要一些额外的语法来保证编译器正确工作。

4 其他语言的处理方式

这套规则查看下总感觉没那么智能,而且有传言本来C++ 11是要优化这个问题的[2],但似乎现在其实没有太大改变。那么我们来看看同样具有template语法的Java是如何处理这个问题的吧。

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这样的关键字,编译,运行,成功获得结果:

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, pages.cs.wisc.edu/~driscoll/t… [3] 《C++ Primer 5e》, Stanley B. Lippman, 《电子工业出版社》, 2013.9