Browse Source

重组3.1为3.1.1 - 3.1.3节。

wuye9036 9 years ago
parent
commit
743778d9d6
1 changed files with 58 additions and 7 deletions
  1. 58 7
      ReadMe.md

+ 58 - 7
ReadMe.md

@@ -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>`体现出来的。
+
+这也是为什么在特化的时候,当所有类型都已经确定,我们就可以抛弃全部的模板参数,写出`template <> struct X<int, float>`这样的形式:因为所有列表中所有参数都确定了,就不需要额外的形式参数了。
 
-其次,作为一个模式匹配,偏特化中展现出来的模式,就是它能被匹配的精髓。比如,`struct X<T, T>`中,要求模板的两个参数必须是相同的类型。而`struct X<T, T*>`,则代表第二个模板类型参数必须是第一个模板类型参数的指针,比如`X<float***, float****>`就能匹配上。当然,除了简单的指针、`const`和`volatile`修饰符,其他的类模板也可以作为偏特化时的“模式”出现,例如示例8,它要求传入同一个类型的`unique_ptr`和`shared_ptr`。
+其次,作为一个模式匹配,偏特化的实参列表中展现出来的“样子”,就是它能被匹配的原因。比如,`struct X<T, T>`中,要求模板的两个参数必须是相同的类型。而`struct X<T, T*>`,则代表第二个模板类型参数必须是第一个模板类型参数的指针,比如`X<float***, float****>`就能匹配上。当然,除了简单的指针、`const`和`volatile`修饰符,其他的类模板也可以作为偏特化时的“模式”出现,例如示例8,它要求传入同一个类型的`unique_ptr`和`shared_ptr`。
 
 对于某些实例化,偏特化的选择并不是唯一的。比如v4的参数是`<float*, float*>`,能够匹配的就有三条规则,1,6和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)