模版编程基础知识
1.编译器匹配问题
例子:
1 template <typename T> class TypeToID 2 { 3 public: 4 static int const NotID = -2; 5 }; 6 7 template <> class TypeToID<float> 8 { 9 public: 10 static int const ID = 1; 11 }; 12 13 void PrintID() 14 { 15 cout << "ID of float: " << TypeToID<float>::ID << endl; // Print "1" 16 cout << "NotID of float: " << TypeToID<float>::NotID << endl; // Error! TypeToID<float>使用的特化的类,这个类的实现没有NotID这个成员。 17 cout << "ID of double: " << TypeToID<double>::ID << endl; // Error! TypeToID<double>是由模板类实例化出来的,它只有NotID,没有ID这个成员。 18 }
类模板和类模板的特化的作用,仅仅是指导编译器选择哪个编译,但是特化之间、特化和它原型的类模板之间,是分别独立实现的。所以如果多个特化、或者特化和对应的类模板有着类似的内容,很不好意思,你得写上若干遍了。
2.类型匹配问题
例子2:
1 template <typename T> // 嗯,需要一个T 2 class TypeToID<T*> // 我要对所有的指针类型特化,所以这里就写T* 3 { 4 public: 5 typedef T SameAsT; 6 static int const ID = 0x80000000; // 用最高位表示它是一个指针 7 }; 8 9 void PrintID() 10 { 11 cout << "ID of float*: " << TypeToID< TypeToID<float*>::SameAsT >::ID << endl; 12 }
这里面,TypeToID<float*>后的T,匹配的是什么呢?
.....
.....
.....
float * 匹配 T * ,而不是float * 匹配 T, 即表示红色的地方,就是特化匹配处。
3。名称查找:I am who I am
名称解析(Name resolution)
1) 模板定义中能够出现以下三类名称:
模板名称、或模板实现中所定义的名称;
和模板参数有关的名称;
模板定义所在的定义域内能看到的名称。
2) 如果名字查找和模板参数有关,那么查找会延期到模板参数全都确定的时候。 …
3) 如果(模板定义内出现的)名字和模板参数无关,那么在模板定义处,就应该找得到这个名字的声明。…
依赖性名称(Dependent names)
1) …(模板定义中的)表达式和类型可能会依赖于模板参数,并且模板参数会影响到名称查找的作用域 … 如果表达式中有操作数依赖于模板参数,那么整个表达式都依赖于模板参数,名称查找延期到模板实例化时进行。并且定义时和实例化时的上下文都会参与名称查找。(依赖性)表达式可以分为类型依赖(类型指模板参数的类型)或值依赖。
类型依赖的表达式
1) 如果成员函数所属的类型是和模板参数有关的,那么这个成员函数中的this就认为是类型依赖的。
非依赖性名称(Non-dependent names)
1) 非依赖性名称在模板定义时使用通常的名称查找规则进行名称查找。
例子:
1 template <typename T> struct X {}; 2 3 template <typename T> struct Y 4 { 5 typedef X<T> ReboundType; // 这里为什么是正确的? 6 typedef typename X<T>::MemberType MemberType2; // 这里的typename是做什么的? 7 typedef UnknownType MemberType3; // 这里为什么会出错? 8 };
依托上面的学术黑话,理解如下:
1 template <typename T> struct Y 2 { 3 // X可以查找到原型; 4 // X<T>是一个依赖性名称,模板定义阶段并不管X<T>是不是正确的。 5 typedef X<T> ReboundType; 6 7 // X可以查找到原型; 8 // X<T>是一个依赖性名称,X<T>::MemberType也是一个依赖性名称; 9 // 所以模板声明时也不会管X模板里面有没有MemberType这回事。 10 typedef typename X<T>::MemberType MemberType2; 11 12 // UnknownType 不是一个依赖性名称 13 // 而且这个名字在当前作用域中不存在,所以直接报错。 14 typedef UnknownType MemberType3; 15 };
唯一的问题就是第二个:typename是做什么的?
对于用户来说,这其实是一个语法噪音。也就是说,其实就算没有它,语法上也说得过去。事实上,某些情况下MSVC的确会在标准需要的时候,不用写typename。但是标准中还是规定了形如 T::MemberType 这样的qualified id 在默认情况下不是一个类型,而是解释为T的一个成员变量MemberType,只有当typename修饰之后才能作为类型出现。
简单来说,如果编译器能在出现的时候知道它的类型,那么就不需要typename
,如果必须要到实例化的时候才能知道它是不是合法,那么定义的时候就把这个名称作为变量而不是类型。
在这里,我举几个例子帮助大家理解typename
的用法,这几个例子已经足以涵盖日常使用:
1 struct A; 2 template <typename T> struct B; 3 template <typename T> struct X 4 { 5 typedef X<T> _A; // 编译器当然知道 X<T> 是一个类型。 6 typedef X _B; // X 等价于 X<T> 的缩写 7 typedef T _C; // T 不是一个类型还玩毛 8 9 // !!!注意我要变形了!!! 10 class Y 11 { 12 typedef X<T> _D; // X 的内部,既然外部高枕无忧,内部更不用说了 13 typedef X<T>::Y _E; // 嗯,这里也没问题,编译器知道Y就是当前的类型, 14 // 这里在VS2015上会有错,需要添加 typename, 15 // Clang 上顺利通过。 16 typedef typename X<T*>::Y _F; // 这个居然要加 typename! 17 // 因为,X<T*>和X<T>不一样哦, 18 // 它可能会在实例化的时候被别的偏特化给抢过去实现了。 19 }; 20 21 typedef A _G; // 嗯,没问题,A在外面声明啦 22 typedef B<T> _H; // B<T>也是一个类型 23 typedef typename B<T>::type _I; // 嗯,因为不知道B<T>::type的信息, 24 // 所以需要typename 25 typedef B<int>::type _J; // B<int> 不依赖模板参数, 26 // 所以编译器直接就实例化(instantiate)了 27 // 但是这个时候,B并没有被实现,所以就出错了 28 };
4.不定长的模板参数
我们写出的模板原型:
template <typename T0, typename T1> struct DoWork;
继而偏特化/特化问题也解决了:
template <> struct DoWork<int, void> {}; // (1) 这是 int 类型的特化
template <> struct DoWork<float, void> {}; // (2) 这是 float 类型的特化
template <> struct DoWork<int, int> {}; // (3) 这是 int, int 类型的特化
显而易见这个解决方案并不那么完美。首先,不管是偏特化还是用户实例化模板的时候,都需要多撰写好几个void
,而且最长的那个参数越长,需要写的就越多;其次,如果我们的DoWork
在程序维护的过程中新加入了一个参数列表更长的实例,那么最悲惨的事情就会发生 —— 原型、每一个偏特化、每一个实例化都要追加上void
以凑齐新出现的实例所需要的参数数量。
所幸模板参数也有一个和函数参数相同的特性:默认实参(Default Arguments)。只需要一个例子,你们就能看明白了goo.gl/TtmcY9
:
template <typename T0, typename T1 = void> struct DoWork;
template <typename T> struct DoWork<T> {};
template <> struct DoWork<int> {};
template <> struct DoWork<float> {};
template <> struct DoWork<int, int> {};
DoWork<int> i;
DoWork<float> f;
DoWork<double> d;
DoWork<int, int> ii;
所有参数不足,即原型中参数T1
没有指定的地方,都由T1自己的默认参数void
补齐了。
但是这个方案仍然有些美中不足之处。
比如,尽管我们默认了所有无效的类型都以void
结尾,所以正确的类型列表应该是类似于<int, float, char, void, void>
这样的形态。但你阻止不了你的用户写出类似于<void, int, void, float, char, void, void>
这样不符合约定的类型参数列表。
其次,假设这段代码中有一个函数,它的参数使用了和类模板相同的参数列表类型,如下面这段代码:
1 template <typename T0, typename T1 = void> struct X 2 { 3 static void call(T0 const& p0, T1 const& p1); // 0 4 }; 5 6 template <typename T0> struct X<T0> 7 { 8 static void call(T0 const& p0); // 1 9 }; 10 11 void foo() 12 { 13 X<int>::call(5); // 调用函数 1 14 X<int, float>::call(5, 0.5f); // 调用函数 0 15 }
那么,每加一个参数就要多写一个偏特化的形式,甚至还要重复编写一些可以共享的实现。
不过不管怎么说,以长参数加默认参数的方式支持变长参数是可行的做法,这也是C++98/03时代的唯一选择。
例如,Boost.Tuple
就使用了这个方法,支持了变长的Tuple:
1 // Tuple 的声明,来自 boost 2 struct null_type; 3 4 template < 5 class T0 = null_type, class T1 = null_type, class T2 = null_type, 6 class T3 = null_type, class T4 = null_type, class T5 = null_type, 7 class T6 = null_type, class T7 = null_type, class T8 = null_type, 8 class T9 = null_type> 9 class tuple; 10 11 // Tuple的一些用例 12 tuple<int> a; 13 tuple<double&, const double&, const double, double*, const double*> b; 14 tuple<A, int(*)(char, int), B(A::*)(C&), C> c; 15 tuple<std::string, std::pair<A, B> > d; 16 tuple<A*, tuple<const A*, const B&, C>, bool, void*> e;
此外,Boost.MPL也使用了这个手法将boost::mpl::vector
映射到boost::mpl::vector _n_
上。但是我们也看到了,这个方案的缺陷很明显:代码臃肿和潜在的正确性问题。此外,过度使用模板偏特化、大量冗余的类型参数也给编译器带来了沉重的负担。
为了缓解这些问题,在C++11中,引入了变参模板(Variadic Template)。我们来看看支持了变参模板的C++11是如何实现tuple的:
template <typename... Ts> class tuple;
是不是一下子简洁了很多!这里的typename... Ts
相当于一个声明,是说Ts
不是一个类型,而是一个不定常的类型列表。同C语言的不定长参数一样,它通常只能放在参数列表的最后。看下面的例子:
1 template <typename... Ts, typename U> class X {}; // (1) error! 2 template <typename... Ts> class Y {}; // (2) 3 template <typename... Ts, typename U> class Y<U, Ts...> {}; // (3) 4 template <typename... Ts, typename U> class Y<Ts..., U> {}; // (4) error!
为什么第(1)条语句会出错呢?(1)是模板原型,模板实例化时,要以它为基础和实例化时的类型实参相匹配。因为C++的模板是自左向右匹配的,所以不定长参数只能结尾。其他形式,无论写作Ts, U
,或者是Ts, V, Us,
,或者是V, Ts, Us
都是不可取的。(4) 也存在同样的问题。
但是,为什么(3)中, 模板参数和(1)相同,都是typename... Ts, typename U
,但是编译器却并没有报错呢?
(3)和(1)不同,它并不是模板的原型,它只是Y
的一个偏特化。回顾我们在之前所提到的,偏特化时,模板参数列表并不代表匹配顺序,它们只是为偏特化的模式提供的声明,也就是说,它们的匹配顺序,只是按照<U, Ts...>
来,而之前的参数只是告诉你Ts
是一个类型列表,而U
是一个类型,排名不分先后。