模版编程基础知识

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, 即表示红色的地方,就是特化匹配处。

扫描二维码关注公众号,回复: 3270956 查看本文章

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是一个类型,排名不分先后。

猜你喜欢

转载自www.cnblogs.com/ypdxcn/p/9686959.html