重组3.1为3.1.1 - 3.1.3节。

This commit is contained in:
wuye9036 2015-12-15 19:11:26 -08:00
parent 6c918c77cd
commit 743778d9d6

View File

@ -763,7 +763,7 @@ int main()
```
这点限制也粉碎了妄图用模板来包办工厂Factory甚至是反射的梦想。尽管在《Modern C++ Design》中别问我为什么老举这本书因为《C++ Templates》和《Generic Programming》我只是囫囵吞枣读过基本不记得了)大量运用模板来简化工厂方法同时C++11和14中的一些机制如Vardric Template更是让这一问题的解决更加彻底。但无论如何光靠模板你就是写不出依靠类名或者ID变量产生类型实例的代码。
这点限制也粉碎了妄图用模板来包办工厂Factory甚至是反射的梦想。尽管在《Modern C++ Design》中别问我为什么老举这本书因为《C++ Templates》和《Generic Programming》我只是囫囵吞枣读过基本不记得了)大量运用模板来简化工厂方法同时C++11和14中的一些机制如Variadic Template更是让这一问题的解决更加彻底。但无论如何直到C++11/14光靠模板你就是写不出依靠类名或者ID变量产生类型实例的代码。
所以说,从能力上来看,模板能做的事情都是编译期完成的。编译期完成的意思就是,当你编译一个程序的时候,所有的量就都已经确定了。比如下面的这个例子:
@ -1632,6 +1632,9 @@ template <typename T> struct X {
## 3 深入理解特化与偏特化
###3.1 正确的理解偏特化
####3.1.1 偏特化与函数重载的比较
在前面的章节中,我们介绍了偏特化的形式、也介绍了简单的用例。因为偏特化和函数重载存在着形式上的相似性,因此初学者便会借用重载的概念,来理解偏特化的行为。只是,重载和偏特化尽管相似但仍有差异。
我们来先看一个函数重载的例子:
@ -1711,7 +1714,7 @@ DoWork<int> i; // (3)
```C++
template <typename T, typename U> struct X ; // 0
// 原型有两个类型参数
// 所以下面的这些偏特化的“小尾巴”
// 所以下面的这些偏特化的“小尾巴”(实参列表)
// 也需要两个类型参数对应
template <typename T> struct X<T, T > {}; // 1
template <typename T> struct X<T*, T > {}; // 2
@ -1736,15 +1739,21 @@ X<int*, int> v7;
X<double*, double> v8;
```
在上面这段例子中,有几个值得注意之处。首先,偏特化时的模板参数,和原型的模板参数没有任何关系,它只为偏特化这一句服务。这也是为什么在特化的时候所有类型都确定了,我们就可以抛弃全部的模板参数,写出`template <> struct X<int, float>`这样的形式
在上面这段例子中,有几个值得注意之处。首先,偏特化时的模板参数,和原型的模板参数没有任何关系,和原型不同,它的顺序完全不影响模式匹配的顺序,它只是偏特化模式,如`<U, int>`中`U`的声明,真正的模式,是由`<U, int>`体现出来的
其次,作为一个模式匹配,偏特化中展现出来的模式,就是它能被匹配的精髓。比如,`struct X<T, T>`中,要求模板的两个参数必须是相同的类型。而`struct X<T, T*>`,则代表第二个模板类型参数必须是第一个模板类型参数的指针,比如`X<float***, float****>`就能匹配上。当然,除了简单的指针、`const`和`volatile`修饰符其他的类模板也可以作为偏特化时的“模式”出现例如示例8它要求传入同一个类型的`unique_ptr`和`shared_ptr`。
这也是为什么在特化的时候,当所有类型都已经确定,我们就可以抛弃全部的模板参数,写出`template <> struct X<int, float>`这样的形式:因为所有列表中所有参数都确定了,就不需要额外的形式参数了。
其次,作为一个模式匹配,偏特化的实参列表中展现出来的“样子”,就是它能被匹配的原因。比如,`struct X<T, T>`中,要求模板的两个参数必须是相同的类型。而`struct X<T, T*>`,则代表第二个模板类型参数必须是第一个模板类型参数的指针,比如`X<float***, float****>`就能匹配上。当然,除了简单的指针、`const`和`volatile`修饰符其他的类模板也可以作为偏特化时的“模式”出现例如示例8它要求传入同一个类型的`unique_ptr`和`shared_ptr`。
对于某些实例化偏特化的选择并不是唯一的。比如v4的参数是`<float*, float*>`能够匹配的就有三条规则16和7。很显然6还是比7好一些因为能多匹配一个指针。但是1和6就很难说清楚谁更好了。一个说明了两者类型相同另外一个则说明了两者都是指针。所以在这里编译器也没办法决定使用那个只好爆出了编译器错误。
其他的示例可以先自己推测一下, 再去编译器上尝试一番 (http://goo.gl/9UVzje)。
不过这个时候也许你还不死心。有没有一种办法能够让最初的例子`DoWork`像重载一样的支持多个参数呢?答案当然是肯定的。
#### 3.1.2 不定长的模板参数
不过这个时候也许你还不死心。有没有一种办法能够让例子`DoWork`像重载一样,支持对长度不一的参数列表分别偏特化/特化呢?
答案当然是肯定的。
首先,首先我们要让模板实例化时的模板参数统一到相同形式上。逆向思维一下,虽然两个类型参数我们很难缩成一个参数,但是我们可以通过添加额外的参数,把一个扩展成两个呀。比如这样:
@ -1790,7 +1799,7 @@ DoWork<int, int> ii;
但是这个方案仍然有些美中不足之处。
比如,尽管我们默认了所有无效的类型都以`void`结尾,所以正确的类型列表应该是类似于`<int, float, char, void, void>`这样的形态。但你阻止不了你的用户写出类似于`<void, int, void, float, char, void, void>`这样奇怪的类型参数列表。
比如,尽管我们默认了所有无效的类型都以`void`结尾,所以正确的类型列表应该是类似于`<int, float, char, void, void>`这样的形态。但你阻止不了你的用户写出类似于`<void, int, void, float, char, void, void>`这样不符合约定的类型参数列表。
其次,假设这段代码中有一个函数,它的参数使用了和类模板相同的参数列表类型,如下面这段代码:
@ -1811,9 +1820,51 @@ void foo(){
那么,每加一个参数就要多写一个偏特化的形式,甚至还要重复编写一些可以共享的实现。
为了解决这几个问题在C++11中引入了变参模板Variadic Template
不过不管怎么说以长参数加默认参数的方式支持变长参数是可行的做法这也是C++98/03时代的唯一选择
例如,[Boost.Tuple](https://github.com/boostorg/tuple/blob/develop/include/boost/tuple/detail/tuple_basic.hpp)就使用了这个方法支持了变长的Tuple
```C++
// Tuple 的声明,来自 boost
template <
class T0 = null_type, class T1 = null_type, class T2 = null_type,
class T3 = null_type, class T4 = null_type, class T5 = null_type,
class T6 = null_type, class T7 = null_type, class T8 = null_type,
class T9 = null_type>
class tuple;
// Tuple的一些用例
tuple<int> a;
tuple<double&, const double&, const double, double*, const double*> b;
tuple<A, int(*)(char, int), B(A::*)(C&), C> c;
tuple<std::string, std::pair<A, B> > d;
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的
```C++
template <typename... Ts> class tuple;
```
是不是一下子简洁了很多!这里的`typename... Ts`相当于一个声明,是说`Ts`不是一个类型而是一个不定常的类型列表。同C语言的不定长参数一样它通常只能放在参数列表的最后。看下面的例子
```C++
template <typename... Ts, typename U> class X {}; // (1) error!
template <typename... Ts> class Y {}; // (2)
template <typename... Ts, typename U> class Y<U, Ts...> {}; // (3)
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`是一个类型,排名不分先后。
####3.1.3 模板的默认实参
###3.2 后悔药SFINAE
###3.3 实战单元获得类型的属性——类型萃取Type Traits