diff --git a/ReadMe.md b/ReadMe.md index 14993fd..1588b79 100644 --- a/ReadMe.md +++ b/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 struct X { ## 3 深入理解特化与偏特化 ###3.1 正确的理解偏特化 + +####3.1.1 偏特化与函数重载的比较 + 在前面的章节中,我们介绍了偏特化的形式、也介绍了简单的用例。因为偏特化和函数重载存在着形式上的相似性,因此初学者便会借用重载的概念,来理解偏特化的行为。只是,重载和偏特化尽管相似但仍有差异。 我们来先看一个函数重载的例子: @@ -1711,7 +1714,7 @@ DoWork i; // (3) ```C++ template struct X ; // 0 // 原型有两个类型参数 - // 所以下面的这些偏特化的“小尾巴” + // 所以下面的这些偏特化的“小尾巴”(实参列表) // 也需要两个类型参数对应 template struct X {}; // 1 template struct X {}; // 2 @@ -1736,15 +1739,21 @@ X v7; X v8; ``` -在上面这段例子中,有几个值得注意之处。首先,偏特化时的模板参数,和原型的模板参数没有任何关系,它只为偏特化这一句服务。这也是为什么在特化的时候所有类型都确定了,我们就可以抛弃全部的模板参数,写出`template <> struct X`这样的形式。 +在上面这段例子中,有几个值得注意之处。首先,偏特化时的模板参数,和原型的模板参数没有任何关系,和原型不同,它的顺序完全不影响模式匹配的顺序,它只是偏特化模式,如``中`U`的声明,真正的模式,是由``体现出来的。 -其次,作为一个模式匹配,偏特化中展现出来的模式,就是它能被匹配的精髓。比如,`struct X`中,要求模板的两个参数必须是相同的类型。而`struct X`,则代表第二个模板类型参数必须是第一个模板类型参数的指针,比如`X`就能匹配上。当然,除了简单的指针、`const`和`volatile`修饰符,其他的类模板也可以作为偏特化时的“模式”出现,例如示例8,它要求传入同一个类型的`unique_ptr`和`shared_ptr`。 +这也是为什么在特化的时候,当所有类型都已经确定,我们就可以抛弃全部的模板参数,写出`template <> struct X`这样的形式:因为所有列表中所有参数都确定了,就不需要额外的形式参数了。 + +其次,作为一个模式匹配,偏特化的实参列表中展现出来的“样子”,就是它能被匹配的原因。比如,`struct X`中,要求模板的两个参数必须是相同的类型。而`struct X`,则代表第二个模板类型参数必须是第一个模板类型参数的指针,比如`X`就能匹配上。当然,除了简单的指针、`const`和`volatile`修饰符,其他的类模板也可以作为偏特化时的“模式”出现,例如示例8,它要求传入同一个类型的`unique_ptr`和`shared_ptr`。 对于某些实例化,偏特化的选择并不是唯一的。比如v4的参数是``,能够匹配的就有三条规则,1,6和7。很显然,6还是比7好一些,因为能多匹配一个指针。但是1和6,就很难说清楚谁更好了。一个说明了两者类型相同;另外一个则说明了两者都是指针。所以在这里,编译器也没办法决定使用那个,只好爆出了编译器错误。 其他的示例可以先自己推测一下, 再去编译器上尝试一番 (http://goo.gl/9UVzje)。 -不过这个时候也许你还不死心。有没有一种办法能够让最初的例子`DoWork`像重载一样的支持多个参数呢?答案当然是肯定的。 +#### 3.1.2 不定长的模板参数 + +不过这个时候也许你还不死心。有没有一种办法能够让例子`DoWork`像重载一样,支持对长度不一的参数列表分别偏特化/特化呢? + +答案当然是肯定的。 首先,首先我们要让模板实例化时的模板参数统一到相同形式上。逆向思维一下,虽然两个类型参数我们很难缩成一个参数,但是我们可以通过添加额外的参数,把一个扩展成两个呀。比如这样: @@ -1790,7 +1799,7 @@ DoWork ii; 但是这个方案仍然有些美中不足之处。 -比如,尽管我们默认了所有无效的类型都以`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 a; +tuple b; +tuple c; +tuple > d; +tuple, bool, void*> e; +``` + +此外,Boost.MPL也使用了这个手法将`boost::mpl::vector`映射到`boost::mpl::vector _n_`上。但是我们也看到了,这个方案的缺陷很明显:代码臃肿和潜在的正确性问题。此外,过度使用模板偏特化、大量冗余的类型参数也给编译器带来了沉重的负担。 + +为了缓解这些问题,在C++11中,引入了变参模板(Variadic Template)。我们来看看支持了变参模板的C++11是如何实现tuple的: + +```C++ +template class tuple; +``` + +是不是一下子简洁了很多!这里的`typename... Ts`相当于一个声明,是说`Ts`不是一个类型,而是一个不定常的类型列表。同C语言的不定长参数一样,它通常只能放在参数列表的最后。看下面的例子: + +```C++ +template class X {}; // (1) error! +template class Y {}; // (2) +template class Y {}; // (3) +template class Y {}; // (4) error! +``` + +为什么第(1)条语句会出错呢?(1)是模板原型,模板实例化时,要以它为基础和实例化时的类型实参相匹配。因为C++的模板是自左向右匹配的,所以不定长参数只能结尾。其他形式,无论写作`Ts, U`,或者是`Ts, V, Us,`,或者是`V, Ts, Us`都是不可取的。(4) 也存在同样的问题。 + +但是,为什么(3)中, 模板参数和(1)相同,都是`typename... Ts, typename U`,但是编译器却并没有报错呢? + +答案在这一节的早些时候。(3)和(1)不同,它并不是模板的原型,它只是`Y`的一个偏特化。回顾我们在之前所提到的,偏特化时,模板参数列表并不代表匹配顺序,它们只是为偏特化的模式提供的声明,也就是说,它们的匹配顺序,只是按照``来,而之前的参数只是告诉你`Ts`是一个类型列表,而`U`是一个类型,排名不分先后。 + +####3.1.3 模板的默认实参 ###3.2 后悔药:SFINAE ###3.3 实战单元:获得类型的属性——类型萃取(Type Traits)