From 7819d831fdc4dac3a2b8b486ddec1d58e1956593 Mon Sep 17 00:00:00 2001 From: Ye WU Date: Wed, 2 Dec 2015 16:22:16 -0800 Subject: [PATCH 01/35] =?UTF-8?q?=E7=AC=AC=E4=BA=8C=E7=AB=A0=E7=BB=93?= =?UTF-8?q?=E6=9D=9F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ReadMe.md | 124 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 101 insertions(+), 23 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index d37900e..5315ce3 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -949,8 +949,8 @@ class AddFloatOrMulInt // 以及模板参数列表 template class AddFloatOrMulInt; -// 最后,模板参数列表里面填什么?因为原型的T已经被int取代了。所以这里就不能放任何额外的参数了。 -// 所以这里要放空。 +// 最后,模板参数列表里面填什么?因为原型的T已经被int取代了。所以这里就不能也不需要放任何额外的参数了。 +// 所以这里放空。 template <> class AddFloatOrMulInt { // ... 针对Int的实现 ... @@ -1090,8 +1090,8 @@ void copy(void* dst, void const* src, size_t elemSize, size_t elemCount, void (* ``` 为什么要提供copyElem,是因为可能有些struct需要深拷贝,所以得用特殊的copy函数。这个在C++98/03里面就体现为拷贝构造和赋值函数。 -但是不管怎么搞,因为这个函数的参数只是`void*`而已,当你使用了错误的elemSize,或者传入了错误的copyElem,就必须要到运行的时候才有可能看出来。注意,这还只是有可能而已。 +但是不管怎么搞,因为这个函数的参数只是`void*`而已,当你使用了错误的elemSize,或者传入了错误的copyElem,就必须要到运行的时候才有可能看出来。注意,这还只是有可能而已。 那么C++有了模板后,能否既能匹配任意类型的指针,同时又保留了类型信息呢?答案是显然的。至于怎么写,那就得充分发挥你的直觉了: @@ -1115,6 +1115,7 @@ void copy(T* dst, T const* src, size_t elemCount); ``` 编译一下,咦,居然通过了。看来这里的语法与我们以前学到的知识并没有什么不同。这也是语言设计最重要的一点原则:一致性。它可以让你辛辛苦苦体验到的规律不至于白费。 + 最后就是实现: ``` C++ @@ -1387,7 +1388,7 @@ template struct X { 接下来,我们就来解决2.3.1节中留下的几个问题。 -先看第四个问题。为什么MSVC中,模板函数的定义内不管填什么编译器都不报错?因为MSVC在分析模板定义时没有做任何事情。至于为啥连“大王叫我来巡山”都能过得去,这是C++语法/语义分析的特殊性导致的。 +先看第四个问题。为什么MSVC中,模板函数的定义内不管填什么编译器都不报错?因为MSVC在分析模板中成员函数定义时没有做任何事情。至于为啥连“大王叫我来巡山”都能过得去,这是C++语法/语义分析的特殊性导致的。 C++是个非常复杂的语言,以至于它的编译器,不可能通过词法-语法-语义多趟分析清晰分割。因为它的语义将会直接干扰到语法: ```C++ @@ -1399,7 +1400,7 @@ void foo(){ 甚至词法分析也会受到语义的干扰,C++11中才明确被修正的`vector>`,就因为`>>`被误解为右移或流操作符,而导致某些编译器上的错误。因此,在语义没有确定之前,连语法都没有分析的价值。 -大约是基于如此考量,为了偷懒,MSVC将包括所有的语法/语义分析工作都挪到了第二个Phase,于是乎连带着语法分析都送进了第二个阶段。符合标准么?显然不符合。 +大约是基于如此考量,为了偷懒,MSVC将包括所有模板成员函数的语法/语义分析工作都挪到了第二个Phase,于是乎连带着语法分析都送进了第二个阶段。符合标准么?显然不符合。 但是这里值得一提的是,MSVC的做法和标准相比,虽然投机取巧,但并非有弊无利。我们来先说一说坏处。考虑以下例子: ```C++ @@ -1483,9 +1484,9 @@ error: no type named 'MemberType' in 'X' ```C++ struct A; template struct X { - void foo(T v) { - A a; - a.v = v; + int v; + void convertTo(A& a) { + a.v = v; // 这里需要A的实现 } }; @@ -1512,16 +1513,13 @@ error: variable has incomplete type 'A' ```C++ struct A; template struct X { - void foo(T v) { - A a; - a.v = v; - } + int v; + void convertTo(A& a); }; struct A { int v; }; -template void X::foo(T v) { - A a; +template void X::convertTo(A& a) { a.v = v; } @@ -1531,18 +1529,97 @@ void main() { } ``` -但是其实我们知道,`foo`要到实例化之后,才需要真正的做语义分析。在MSVC上,因为函数实现就是到模板实例化时才处理的,所以这个例子是完全正常工作的。 - -在实际应用中,我们经常既希望把模板类成员函数的声明和实现放到一起,因为模板函数看不到实现也很难调用;又希望一般类型可以声明定义分离,把类型定义隐藏到源文件中,以完成声明实现分离。 - -此时如果编译器是符合标准的,我们只能将模板头文件拆分成``和``两个部分,并按照顺序引用两个文件。但是在MSVC中就可以直接将模板函数的实现,和一般类型的声明放在一起,反而更加简单清晰。 +但是其实我们知道,`foo`要到实例化之后,才需要真正的做语义分析。在MSVC上,因为函数实现就是到模板实例化时才处理的,所以这个例子是完全正常工作的。因此在上面这个例子中,MSVC的实现要比标准更加易于写和维护,是不是有点写Java/C#那种声明实现都在同一处的清爽感觉了呢! 扩展阅读: [The Dreaded Two-Phase Name Lookup][2] -###2.4 函数模板的重载、参数匹配、特化与部分特化 -###2. 技巧单元:模板与继承 +#2.3.3 “多余的” typename 关键字 -## 3 拿起特化的武器,去写程序吧! +到了这里,2.3.1 中提到的四个问题,还有三个没有解决: + +```C++ +template struct X {}; + +template struct Y +{ + typedef X ReboundType; // 这里为什么是正确的? + typedef typename X::MemberType MemberType2; // 这里的typename是做什么的? + typedef UnknownType MemberType3; // 这里为什么会出错? +}; +``` + +我们运用我们2.3.2节中学习到的标准,来对Y内部做一下分析: + +``` +template struct Y +{ + // X可以查找到原型; + // X是一个依赖性名称,模板定义阶段并不管X是不是正确的。 + typedef X ReboundType; + + // X可以查找到原型; + // X是一个依赖性名称,X::MemberType也是一个依赖性名称; + // 所以模板声明时也不会管X模板里面有没有MemberType这回事。 + typedef typename X::MemberType MemberType2; + + // UnknownType 不是一个依赖性名称 + // 而且这个名字在当前作用域中不存在,所以直接报错。 + typedef UnknownType MemberType3; +}; +``` + +下面,唯一的问题就是第二个:`typename`是做什么的? + +对于用户来说,这其实是一个语法噪音。也就是说,其实就算没有它,语法上也说得过去。事实上,某些情况下MSVC的确会在标准需要的时候,不用写`typename`。但是标准中还是规定了形如 `T::MemberType` 这样的`qualified id` 在默认情况下不是一个类型,而是解释为`T`的一个成员变量`MemberType`,只有当`typename`修饰之后才能作为类型出现。 + +事实上,标准对`typename`的使用规定极为复杂,也算是整个模板中的难点之一。如果想了解所有的标准,需要阅读标准14.6节下2-7条,以及14.6.2.1第一条中对于`current instantiation`的解释。 + +简单来说,如果编译器能在出现的时候知道它的类型,那么就不需要`typename`,如果必须要到实例化的时候才能知道它是不是合法,那么定义的时候就把这个名称作为变量而不是类型。 + +在这里,我举几个例子帮助大家理解`typename`的用法,这几个例子已经足以涵盖日常使用[(预览)][3]: + +```C++ +struct A; +template struct B; +template struct X { + typedef X _A; // 编译器当然知道 X 是一个类型。 + typedef X _B; // X 等价于 X 的缩写 + typedef T _C; // T 不是一个类型还玩毛 + + // !!!注意我要变形了!!! + class Y { + typedef X _D; // X 的内部,既然外部高枕无忧,内部更不用说了 + typedef X::Y _E; // 嗯,这里也没问题,编译器知道Y就是当前的类型, + // 这里在VS2015上会有错,需要添加 typename, + // Clang 上顺利通过。 + typedef typename X::Y _F; // 这个居然要加 typename! + // 因为,X和X不一样哦, + // 它可能会在实例化的时候被别的偏特化给抢过去实现了。 + }; + + typedef A _G; // 嗯,没问题,A在外面声明啦 + typedef B _H; // B也是一个类型 + typedef typename B::type _I; // 嗯,因为不知道B::type的信息, + // 所以需要typename + typedef B::type _J; // B 不依赖模板参数, + // 所以编译器直接就实例化(instantiate)了 + // 但是这个时候,B并没有被实现,所以就出错了 +}; +``` + +### 2.4 本章小结 + +这一张是写作中最艰难的一章,中间停滞了将近一年。因为要说清楚C++模板中一些语法噪音和设计决议并不是一件轻松的事情。不过通过这一章的学习,我们知道了下面这几件事情: + +1. **部分特化/偏特化** 和 **特化** 相当于是模板实例化过程中的`if-then-else`。这使得我们根据不同类型,选择不同实现的需求得以实现; + +2. 在 2.3.3 一节我们插入了C++模板中最难理解的内容之一:名称查找。名称查找是语义分析的一个环节,模板内书写的 **变量声明**、**typedef**、**类型名称** 甚至 **类模板中成员函数的实现** 都要符合名称查找的规矩才不会出错; + +3. C++编译器对语义的分析的原则是“大胆假设,小心求证”:在能求证的地方尽量求证 —— 比如两段式名称查找的第一阶段;无法检查的地方假设你是正确的 —— 比如`typedef typename A::MemberType _X;`在模板定义时因为`T`不明确不会轻易判定这个语句的死刑。 + +从下一章开始,我们将进入元编程环节。我们将使用大量的示例,一方面帮助巩固大家学到的模板知识,一方面也会引导大家使用函数式思维去解决常见的问题。 + +## 3 拿起武器,去写程序吧! ###3.1 利用模板特化规则实现If-Then-Else与Switch-Case ###3.2 特化可以有多个选择:替换失败并不是一个错误,只是一种可能 ###3.3 技巧单元:获得类型的属性——类型萃取(Type Traits) @@ -1580,4 +1657,5 @@ alexandrescu 关于 min max 的讨论:《再谈Min和Max》 [1]: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf - [2]: http://blog.llvm.org/2009/12/dreaded-two-phase-name-lookup.html \ No newline at end of file + [2]: http://blog.llvm.org/2009/12/dreaded-two-phase-name-lookup.html + [3]: https://goo.gl/zCRNYx \ No newline at end of file From ebfe1dfb910df2481550b68c1159eca8e966f057 Mon Sep 17 00:00:00 2001 From: wuye9036 Date: Wed, 2 Dec 2015 17:45:31 -0800 Subject: [PATCH 02/35] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E4=BA=86=E4=B8=80?= =?UTF-8?q?=E4=B8=AA=E7=A4=BA=E4=BE=8B=E7=9A=84=E6=A0=BC=E5=BC=8F=E9=97=AE?= =?UTF-8?q?=E9=A2=98=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ReadMe.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index 5315ce3..34f1aa2 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1550,7 +1550,7 @@ template struct Y 我们运用我们2.3.2节中学习到的标准,来对Y内部做一下分析: -``` +```C++ template struct Y { // X可以查找到原型; @@ -1658,4 +1658,4 @@ alexandrescu 关于 min max 的讨论:《再谈Min和Max》 [1]: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf [2]: http://blog.llvm.org/2009/12/dreaded-two-phase-name-lookup.html - [3]: https://goo.gl/zCRNYx \ No newline at end of file + [3]: https://goo.gl/zCRNYx From d3cc0592182cef56e54a56d4955983cace5d1643 Mon Sep 17 00:00:00 2001 From: wuye9036 Date: Wed, 2 Dec 2015 18:32:49 -0800 Subject: [PATCH 03/35] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E4=BA=862.3.3=E6=A0=87?= =?UTF-8?q?=E9=A2=98=E7=9A=84=E6=A0=BC=E5=BC=8F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ReadMe.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ReadMe.md b/ReadMe.md index 34f1aa2..310b3a8 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1533,7 +1533,7 @@ void main() { 扩展阅读: [The Dreaded Two-Phase Name Lookup][2] -#2.3.3 “多余的” typename 关键字 +####2.3.3 “多余的” typename 关键字 到了这里,2.3.1 中提到的四个问题,还有三个没有解决: From a46f244c9637434040930d7eef2a1a7b30598d71 Mon Sep 17 00:00:00 2001 From: wuye9036 Date: Wed, 2 Dec 2015 19:12:36 -0800 Subject: [PATCH 04/35] =?UTF-8?q?=E4=BF=AE=E8=AE=A2=E4=BA=862.1=E4=B8=AD?= =?UTF-8?q?=E5=AE=B9=E6=98=93=E5=AF=BC=E8=87=B4=E6=AD=A7=E4=B9=89=E7=9A=84?= =?UTF-8?q?=E4=BE=8B=E5=AD=90=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit close #5. --- ReadMe.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index 310b3a8..6997359 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -618,9 +618,9 @@ typedef Stack StackFloat; ``` Int8,16: N/A -Int32 : VInt32Mul(int32 * 4, int32 * 4) -Int64 : VInt64Mul(int64 * 2, int64 * 2) -Float : VInt64Mul(float * 2, float * 2) +Int32 : VInt32Mul(int32x4, int32x4) +Int64 : VInt64Mul(int64x4, int64x4) +Float : VInt64Mul(floatx2, floatx2) ``` 所以对于Int8和Int16,我们需要提升到Int32,而Int32和Int64,各自使用自己的指令。所以我们需要实现下的逻辑: @@ -641,10 +641,10 @@ for(v4a, v4b : vectorsA, vectorsB) 嗯,聪明你果然想到了,重载也可以解决这个问题。 ``` C++ -GenericMul(int8 * 4, int8 * 4); -GenericMul(int16 * 4, int16 * 4); -GenericMul(int32 * 4, int32 * 4); -GenericMul(int64 * 4, int64 * 4); +GenericMul(int8x4, int8x4); +GenericMul(int16x4, int16x4); +GenericMul(int32x4, int32x4); +GenericMul(int64x4, int64x4); // 其它 Generic Mul ... for(v4a, v4b : vectorsA, vectorsB) From c9c3521eb9b8294b64752b2824226d29ff0354f4 Mon Sep 17 00:00:00 2001 From: wuye9036 Date: Thu, 3 Dec 2015 00:45:23 -0800 Subject: [PATCH 05/35] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=E4=B8=80?= =?UTF-8?q?=E5=A4=84=E7=94=A8=E8=AF=AD=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ReadMe.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ReadMe.md b/ReadMe.md index 6997359..c9c5b66 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1396,7 +1396,7 @@ void foo(){ A b; } ``` -在这段简短的代码中,就包含了两个歧义的可能,一是`A`是模板,于是`A`是一个实例化的类型,`b`是变量,另外一种就是关系表达式,`((A < T) > b)`。 +在这段简短的代码中,就包含了两个歧义的可能,一是`A`是模板,于是`A`是一个实例化的类型,`b`是变量,另外一种是比较表达式(Comparison Expression)的组合,`((A < T) > b)`。 甚至词法分析也会受到语义的干扰,C++11中才明确被修正的`vector>`,就因为`>>`被误解为右移或流操作符,而导致某些编译器上的错误。因此,在语义没有确定之前,连语法都没有分析的价值。 From baf962c80fddcb33258d0c7e7c05200e5fe79f23 Mon Sep 17 00:00:00 2001 From: wuye9036 Date: Sun, 6 Dec 2015 22:51:07 -0800 Subject: [PATCH 06/35] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=E5=90=8E?= =?UTF-8?q?=E5=87=A0=E7=AB=A0=E7=9A=84=E5=86=99=E4=BD=9C=E8=AE=A1=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ReadMe.md | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index c9c5b66..048f05d 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1619,33 +1619,33 @@ template struct X { 从下一章开始,我们将进入元编程环节。我们将使用大量的示例,一方面帮助巩固大家学到的模板知识,一方面也会引导大家使用函数式思维去解决常见的问题。 -## 3 拿起武器,去写程序吧! -###3.1 利用模板特化规则实现If-Then-Else与Switch-Case -###3.2 特化可以有多个选择:替换失败并不是一个错误,只是一种可能 -###3.3 技巧单元:获得类型的属性——类型萃取(Type Traits) +## 3 深入理解特化 + +###3.1 利用偏特化实现If-Then-Else +###3.2 正确的理解偏特化 +###3.3 后悔药:SFINAE +###3.4 技巧单元:获得类型的属性——类型萃取(Type Traits) ## 4 用模板写程序吧!骚年! ###4.1 模板上的递归 ###4.2 将循环变成递归,将分支变成递归,将一切变成递归 ###4.3 实战单元:元编程的Fibonacci数列 -###4.4 技巧单元:typename与template的另一种用法 -###4.5 实战单元:撰写你自己的元编程“函数”库 -###4.6 实战单元:实现元编程上的数据结构——以Vector为例 -## 5 关于模板,你还需要知道的其它常识 -###5.1 类中类:灵活的模板定义 -###5.2 Template-Template Class -###5.3 技巧单元:高阶函数——从函数到函数的组合 -###5.4 实战单元:STL中的Allocator Rebinder -###5.5 像看堆栈一样的看出错信息 -###5.6 模板的症结:易于实现,难于完美 +## 5 元编程下的算法 +###5.1 列表与数组 +###5.2 字典结构 +###5.3 “快速”排序 + +## 6 关于模板,你还需要知道的其它常识 +###6.1 类中类:灵活的模板定义 +###6.2 Template-Template Class +###6.3 技巧单元:高阶函数——从函数到函数的组合 +###6.4 实战单元:STL中的Allocator Rebinder +###6.5 像看堆栈一样的看出错信息 +###6.6 模板的症结:易于实现,难于完美 alexandrescu 关于 min max 的讨论:《再谈Min和Max》 -## 6 C++11的新特性 -###6.1 变参模板 -###6.2 Lambda与模板程序 - ## 7 模板的威力:从foreach, transform到Linq ###7.1 Foreach与Transform ###7.2 Reactor风格的编程 @@ -1655,7 +1655,6 @@ alexandrescu 关于 min max 的讨论:《再谈Min和Max》 ## 8 结语:讨论有益,争端无用 - [1]: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf [2]: http://blog.llvm.org/2009/12/dreaded-two-phase-name-lookup.html [3]: https://goo.gl/zCRNYx From 56dccfc13304188c97afc3634ff8065d3145c023 Mon Sep 17 00:00:00 2001 From: wuye9036 Date: Sun, 6 Dec 2015 22:57:33 -0800 Subject: [PATCH 07/35] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=E7=AC=AC?= =?UTF-8?q?=E4=B8=89=E7=AB=A0=E7=9A=84=E5=86=99=E4=BD=9C=E8=AE=A1=E5=88=92?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ReadMe.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index 048f05d..a8a2283 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1619,12 +1619,11 @@ template struct X { 从下一章开始,我们将进入元编程环节。我们将使用大量的示例,一方面帮助巩固大家学到的模板知识,一方面也会引导大家使用函数式思维去解决常见的问题。 -## 3 深入理解特化 +## 3 深入理解特化与偏特化 -###3.1 利用偏特化实现If-Then-Else -###3.2 正确的理解偏特化 -###3.3 后悔药:SFINAE -###3.4 技巧单元:获得类型的属性——类型萃取(Type Traits) +###3.1 正确的理解偏特化 +###3.2 后悔药:SFINAE +###3.3 实战单元:获得类型的属性——类型萃取(Type Traits) ## 4 用模板写程序吧!骚年! ###4.1 模板上的递归 From 6e39aa6bf5bf779567f0074c0483cbe8d9104e50 Mon Sep 17 00:00:00 2001 From: wuye9036 Date: Wed, 9 Dec 2015 13:19:37 -0800 Subject: [PATCH 08/35] Update ReadMe.md --- ReadMe.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index a8a2283..bed0af5 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1369,7 +1369,7 @@ template foo(T& v0, C& v1){ int a; struct B { int v; } template struct X { - B b; // B 是第三类名字,b 是第二类 + B b; // B 是第三类名字,b 是第一类 T t; // T 是第二类 X* anthor; // X 这里代指 X,第一类 typedef int Y; // int 是第三类 @@ -1640,7 +1640,7 @@ template struct X { ###6.2 Template-Template Class ###6.3 技巧单元:高阶函数——从函数到函数的组合 ###6.4 实战单元:STL中的Allocator Rebinder -###6.5 像看堆栈一样的看出错信息 +###6.5 更好的编译器,更友善的出错信息 ###6.6 模板的症结:易于实现,难于完美 alexandrescu 关于 min max 的讨论:《再谈Min和Max》 @@ -1650,6 +1650,7 @@ alexandrescu 关于 min max 的讨论:《再谈Min和Max》 ###7.2 Reactor风格的编程 ###7.3 Reactor与Linq ###7.4 Linq的C++实践 +###7.5 Boost.Hana ###7.5 更高更快更强:从Linq到FP ## 8 结语:讨论有益,争端无用 From 914d3e7915dc09f93c5bd3d04ce4f08b735685e2 Mon Sep 17 00:00:00 2001 From: wuye9036 Date: Sun, 13 Dec 2015 02:07:06 -0800 Subject: [PATCH 09/35] =?UTF-8?q?=E6=92=B0=E5=86=99=E4=BA=863.1=E8=8A=82?= =?UTF-8?q?=E9=83=A8=E5=88=86=E5=86=85=E5=AE=B9=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ReadMe.md | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/ReadMe.md b/ReadMe.md index bed0af5..aa99ead 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1622,6 +1622,117 @@ template struct X { ## 3 深入理解特化与偏特化 ###3.1 正确的理解偏特化 +在前面的章节中,我们介绍了偏特化的形式、也介绍了简单的用例。因为偏特化和函数重载存在着形式上的相似性,因此初学者便会借用重载的概念,来理解偏特化的行为。只是,重载和偏特化尽管相似但仍有差异。 + +我们来先看一个函数重载的例子: + +```C++ +void doWork(int); +void doWork(float); +void doWork(int, int); + +void f() { + doWork(0); + doWork(0.5f); + doWork(0, 0); +} +``` + +在这个例子中,我们展现了函数重载可以在两种条件下工作:参数数量相同、类型不同;参数数量不同。 + +仿照重载的形式,我们通过特化机制,试图实现一个模板的“重载”: + +```C++ +template struct DoWork; // (0) 这是原型 + +template <> struct DoWork {}; // (1) 这是 int 类型的"重载" +template <> struct DoWork {}; // (2) 这是 float 类型的"重载" +template <> struct DoWork {}; // (3) 这是 int, int 类型的“重载” + +void f(){ + DoWork i; + DoWork f; + DoWork ii; +} +``` + +这个例子在字面上“看起来”并没有什么问题,可惜编译器在编译的时候仍然提示出错了(http://goo.gl/zI42Zv): + +``` +5 : error: too many template arguments for class template 'DoWork' +template <> struct DoWork {}; // 这是 int, int 类型的“重载” +^ ~~~~ +1 : note: template is declared here +template struct DoWork {}; // 这是原型 +~~~~~~~~~~~~~~~~~~~~~ ^ +``` + +从编译出错的失望中冷静一下,在仔细看看函数特化/偏特化和一般模板的不同之处: + +```C++ +template class X {}; +template class X {}; +// ^^^^ 注意这里 +``` + +对,就是这个``,跟在X后面的小尾巴,决定了第二条语句是第一条语句的跟班。所以,第二条语句即“偏特化”,必须要符合X的基本形式,那就是只有一个参数。这也是为什么`DoWork`尝试以`template <> struct DoWork`的形式偏特化的时候,编译器会提示参数数量过多。 + +另外一方面,在类模板的实例化阶段,它并不会直接去寻找 `template <> struct DoWork`这个小跟班,而是会先找到基本形式,`template struct DoWork;`,然后再去寻找相应的特化。 + +我们以`DoWork i;`为例,尝试复原一下编译器完成整个模板匹配过程的场景,帮助大家理解。看以下示例代码: + +```C++ +template struct DoWork; // (0) 这是原型 + +template <> struct DoWork {}; // (1) 这是 int 类型的"重载" +template <> struct DoWork {}; // (2) 这是 float 类型的"重载" + +DoWork i; // (3) +``` + +1. 编译器分析(0), (1), (2)三句,得知(0)是模板的原型,(1),(2)两句是模板(0)匹配的特例。我们假设有两个字典,第一个字典存储了模板原型,我们称之为`TemplateDict`。第二个字典`TemplateSpecDict`,存储了模板原型所对应的特化/偏特化形式。所以编译器在这三句时,可以视作`TemplateDict.add(DoWork)`,以及 `TemplateSpecDict.get(DoWork).add(int);` 和 `TemplateSpecDict.get(DoWork).add(float);` + +2. (4) 试图以`int`实例化类模板`DoWork`。它会在TemplateDict中,找到`DoWork`,它有一个形式参数`T`接受类型,正好和我们实例化的要求相符合。并且此时`T`被推导为`int`。 + +3. 编译器这个时候就想了,那它会不会有针对int的特化呢?于是就去`TemplateSpecDict`中查找,发现果然有`DoWork`的存在,于是就使用了这个特例。 + +那么根据上面的步骤所展现的基本原理,我们就能知道了,特化形式`struct X`的这个小尾巴,也要和`X`的原型相匹配: + +```C++ +template struct X ; // 0 原型有两个类型参数 + +// 下面的这些偏特化的“小尾巴”也需要两个类型参数对应 +template struct X {}; // 1 +template struct X {}; // 2 +template struct X {}; // 3 +template struct X {}; // 4 +template struct X {}; // 5 +template struct X {}; // 6 +template struct X {}; // 7 + +template struct X, shared_ptr>; // 8 + +// 以下特化,分别对应哪个偏特化的实例? + +X v0; +X v1; +X v2; +X v3; +X v4; +X v5; +X v6; +X v7; +X v8; +``` + +在上面这段例子中,有几个值得注意之处。首先,偏特化时的模板参数,和原型的模板参数没有任何关系,它只为偏特化这一句服务。这也是为什么在特化的时候所有类型都确定了,我们就可以抛弃全部的模板参数,写出`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)。 + ###3.2 后悔药:SFINAE ###3.3 实战单元:获得类型的属性——类型萃取(Type Traits) From f19fd83ef22085e3b023778b534c9eee0bb246a5 Mon Sep 17 00:00:00 2001 From: wuye9036 Date: Sun, 13 Dec 2015 02:12:06 -0800 Subject: [PATCH 10/35] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E4=BA=86=E4=B8=80?= =?UTF-8?q?=E4=BA=9B=E8=A1=8C=E6=96=87=E9=94=99=E8=AF=AF=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ReadMe.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index aa99ead..827e9d9 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1675,7 +1675,7 @@ template class X {}; // ^^^^ 注意这里 ``` -对,就是这个``,跟在X后面的小尾巴,决定了第二条语句是第一条语句的跟班。所以,第二条语句即“偏特化”,必须要符合X的基本形式,那就是只有一个参数。这也是为什么`DoWork`尝试以`template <> struct DoWork`的形式偏特化的时候,编译器会提示参数数量过多。 +对,就是这个``,跟在X后面的小尾巴,决定了第二条语句是第一条语句的跟班。所以,第二条语句,即“偏特化”,必须要符合原型X的基本形式,那就是只有一个参数。这也是为什么`DoWork`尝试以`template <> struct DoWork`的形式偏特化的时候,编译器会提示参数数量过多。 另外一方面,在类模板的实例化阶段,它并不会直接去寻找 `template <> struct DoWork`这个小跟班,而是会先找到基本形式,`template struct DoWork;`,然后再去寻找相应的特化。 @@ -1692,16 +1692,17 @@ DoWork i; // (3) 1. 编译器分析(0), (1), (2)三句,得知(0)是模板的原型,(1),(2)两句是模板(0)匹配的特例。我们假设有两个字典,第一个字典存储了模板原型,我们称之为`TemplateDict`。第二个字典`TemplateSpecDict`,存储了模板原型所对应的特化/偏特化形式。所以编译器在这三句时,可以视作`TemplateDict.add(DoWork)`,以及 `TemplateSpecDict.get(DoWork).add(int);` 和 `TemplateSpecDict.get(DoWork).add(float);` -2. (4) 试图以`int`实例化类模板`DoWork`。它会在TemplateDict中,找到`DoWork`,它有一个形式参数`T`接受类型,正好和我们实例化的要求相符合。并且此时`T`被推导为`int`。 +2. (3) 试图以`int`实例化类模板`DoWork`。它会在`TemplateDict`中,找到`DoWork`,它有一个形式参数`T`接受类型,正好和我们实例化的要求相符合。并且此时`T`被推导为`int`。 -3. 编译器这个时候就想了,那它会不会有针对int的特化呢?于是就去`TemplateSpecDict`中查找,发现果然有`DoWork`的存在,于是就使用了这个特例。 +3. 编译器这个时候就想了,那它会不会有针对`int`的特化呢?于是就去`TemplateSpecDict`中查找,发现果然有`DoWork`的存在,就使用了这个特例。 -那么根据上面的步骤所展现的基本原理,我们就能知道了,特化形式`struct X`的这个小尾巴,也要和`X`的原型相匹配: +那么根据上面的步骤所展现的基本原理,我们随便来几个练习: ```C++ -template struct X ; // 0 原型有两个类型参数 - -// 下面的这些偏特化的“小尾巴”也需要两个类型参数对应 +template struct X ; // 0 + // 原型有两个类型参数 + // 所以下面的这些偏特化的“小尾巴” + // 也需要两个类型参数对应 template struct X {}; // 1 template struct X {}; // 2 template struct X {}; // 3 From 6c02de29335a7662cca6a2075ffeff590286d2fa Mon Sep 17 00:00:00 2001 From: wuye9036 Date: Sun, 13 Dec 2015 14:40:39 -0800 Subject: [PATCH 11/35] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86=E6=8E=A8?= =?UTF-8?q?=E8=8D=90=E7=BC=96=E8=AF=91=E5=99=A8=E4=B8=80=E8=8A=82=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ReadMe.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index 827e9d9..ce4d27f 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -39,7 +39,17 @@ C++之所以变成一门层次丰富、结构多变、语法繁冗的语言, 全文所有为我所撰写的部分,作者均保留所有版权。如果有需要转帖或引用,还请注明出处并告知于我。 -###0.4 意见、建议、喷、补遗、写作计划 +###0.4 推荐编译环境 + +C++编译器众多,且对模板的支持可能存在细微差别。如果没有特别强调,本书行文过程中,使用了下列编译器来测试文中提供的代码和示例: + +* Clang 3.7 (x86) +* Visual Studio 2015 +* GCC 4.9.2 (x86) + +此外,部分复杂实例我们还在文中提供了在线的编译器预览以方便大家阅读和测试。在线编译器参见: http://gcc.godbolt.org/ 。 + +###0.5 意见、建议、喷、补遗、写作计划 * 需增加: * 模板的使用动机。 @@ -1732,7 +1742,9 @@ X v8; 对于某些实例化,偏特化的选择并不是唯一的。比如v4的参数是``,能够匹配的就有三条规则,1,6和7。很显然,6还是比7好一些,因为能多匹配一个指针。但是1和6,就很难说清楚谁更好了。一个说明了两者类型相同;另外一个则说明了两者都是指针。所以在这里,编译器也没办法决定使用那个,只好爆出了编译器错误。 -嘿嘿,自己上编译器看看吧(http://goo.gl/9UVzje)。 +其他的示例可以先自己推测一下, 再去编译器上尝试一番 (http://goo.gl/9UVzje)。 + +再回到第一个例子`DoWork`。 ###3.2 后悔药:SFINAE ###3.3 实战单元:获得类型的属性——类型萃取(Type Traits) From 111110c347df0c2e8bf72c7091ee3cd8e0a8dc0a Mon Sep 17 00:00:00 2001 From: wuye9036 Date: Sun, 13 Dec 2015 20:56:28 -0800 Subject: [PATCH 12/35] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E4=BA=863.1=E8=8A=82?= =?UTF-8?q?=E9=83=A8=E5=88=86=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ReadMe.md | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/ReadMe.md b/ReadMe.md index ce4d27f..33056df 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1744,7 +1744,49 @@ X v8; 其他的示例可以先自己推测一下, 再去编译器上尝试一番 (http://goo.gl/9UVzje)。 -再回到第一个例子`DoWork`。 +不过这个时候也许你还不死心。有没有一种办法能够让最初的例子`DoWork`像重载一样的支持多个参数呢?答案当然是肯定的。 + +首先,首先我们要让模板实例化时的模板参数统一到相同形式上。逆向思维一下,虽然两个类型参数我们很难缩成一个参数,但是我们可以通过添加额外的参数,把一个扩展成两个呀。比如这样: + +```C++ +DoWork i; +DoWork f; +DoWork ii; +``` + +这时,我们就能写出统一的模板原型: + +```C++ +template struct DoWork; +``` + +继而偏特化/特化问题也解决了: + +```C++ +template <> struct DoWork {}; // (1) 这是 int 类型的"重载" +template <> struct DoWork {}; // (2) 这是 float 类型的"重载" +template <> struct DoWork {}; // (3) 这是 int, int 类型的“重载” +``` + +显而易见这个解决方案并不那么完美。首先,不管是偏特化还是用户实例化模板的时候,都需要多撰写好几个`void`,而且最长的那个参数越长,需要写的就越多;其次,如果我们的`DoWork`在程序维护的过程中新加入了一个参数列表更长的实例,那么最悲惨的事情就会发生 —— 原型、每一个偏特化、每一个实例化都要追加上`void`以凑齐新出现的实例所需要的参数数量。 + +所幸模板参数也有一个和函数参数相同的特性:默认实参(Default Arguments)。一个例子你们就看明白了: + +```C++ +template struct DoWork; + +template struct DoWork {}; +template <> struct DoWork {}; +template <> struct DoWork {}; +template <> struct DoWork {}; + +DoWork i; +DoWork f; +DoWork d; +DoWork ii; +``` + +所有参数不足,即原型中参数`T1`没有指定的地方,都由T1自己的默认参数`void`补齐了。 ###3.2 后悔药:SFINAE ###3.3 实战单元:获得类型的属性——类型萃取(Type Traits) From 5fdf0e7448b8a18c907222a478bcd61d13e9c771 Mon Sep 17 00:00:00 2001 From: wuye9036 Date: Sun, 13 Dec 2015 20:58:59 -0800 Subject: [PATCH 13/35] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E4=BA=86=E4=B8=80?= =?UTF-8?q?=E5=A4=84=E8=A1=8C=E6=96=87=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ReadMe.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ReadMe.md b/ReadMe.md index 33056df..41f36d2 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1770,7 +1770,7 @@ template <> struct DoWork {}; // (3) 这是 int, int 类型的“ 显而易见这个解决方案并不那么完美。首先,不管是偏特化还是用户实例化模板的时候,都需要多撰写好几个`void`,而且最长的那个参数越长,需要写的就越多;其次,如果我们的`DoWork`在程序维护的过程中新加入了一个参数列表更长的实例,那么最悲惨的事情就会发生 —— 原型、每一个偏特化、每一个实例化都要追加上`void`以凑齐新出现的实例所需要的参数数量。 -所幸模板参数也有一个和函数参数相同的特性:默认实参(Default Arguments)。一个例子你们就看明白了: +所幸模板参数也有一个和函数参数相同的特性:默认实参(Default Arguments)。只需要一个例子,你们就能看明白了(http://goo.gl/TtmcY9): ```C++ template struct DoWork; From 6c918c77cd907ac753bf6ad4705f98e247f11865 Mon Sep 17 00:00:00 2001 From: wuye9036 Date: Tue, 15 Dec 2015 00:11:22 -0800 Subject: [PATCH 14/35] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E4=BA=86=203.1=20?= =?UTF-8?q?=E8=8A=82=E9=83=A8=E5=88=86=E5=86=85=E5=AE=B9=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ReadMe.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/ReadMe.md b/ReadMe.md index 41f36d2..14993fd 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1788,6 +1788,33 @@ DoWork ii; 所有参数不足,即原型中参数`T1`没有指定的地方,都由T1自己的默认参数`void`补齐了。 +但是这个方案仍然有些美中不足之处。 + +比如,尽管我们默认了所有无效的类型都以`void`结尾,所以正确的类型列表应该是类似于``这样的形态。但你阻止不了你的用户写出类似于``这样奇怪的类型参数列表。 + +其次,假设这段代码中有一个函数,它的参数使用了和类模板相同的参数列表类型,如下面这段代码: + +```C++ +template struct X { + static void call(T0 const& p0, T1 const& p1); // 0 +}; + +template struct X { + static void call(T0 const& p0); // 1 +}; + +void foo(){ + X::call(5); // 调用函数 1 + X::call(5, 0.5f); // 调用函数 0 +} +``` + +那么,每加一个参数就要多写一个偏特化的形式,甚至还要重复编写一些可以共享的实现。 + +为了解决这几个问题,在C++11中,引入了变参模板(Variadic Template)。 + + + ###3.2 后悔药:SFINAE ###3.3 实战单元:获得类型的属性——类型萃取(Type Traits) From 743778d9d63ac74084fdab8cb401ce4a5c73d66a Mon Sep 17 00:00:00 2001 From: wuye9036 Date: Tue, 15 Dec 2015 19:11:26 -0800 Subject: [PATCH 15/35] =?UTF-8?q?=E9=87=8D=E7=BB=843.1=E4=B8=BA3.1.1=20-?= =?UTF-8?q?=203.1.3=E8=8A=82=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ReadMe.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 58 insertions(+), 7 deletions(-) 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) From 3b82848e8f723b9b0f498da6876ccec4a2a8064b Mon Sep 17 00:00:00 2001 From: wuye9036 Date: Tue, 15 Dec 2015 19:19:17 -0800 Subject: [PATCH 16/35] =?UTF-8?q?=E4=BF=AE=E6=94=B9=203.1.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ReadMe.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ReadMe.md b/ReadMe.md index 1588b79..d050baa 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1864,6 +1864,8 @@ template class Y {}; // (4) error! 答案在这一节的早些时候。(3)和(1)不同,它并不是模板的原型,它只是`Y`的一个偏特化。回顾我们在之前所提到的,偏特化时,模板参数列表并不代表匹配顺序,它们只是为偏特化的模式提供的声明,也就是说,它们的匹配顺序,只是按照``来,而之前的参数只是告诉你`Ts`是一个类型列表,而`U`是一个类型,排名不分先后。 +在这里,我们只提到了变长模板参数的声明,如何使用我们将在第四章讲述。 + ####3.1.3 模板的默认实参 ###3.2 后悔药:SFINAE @@ -1895,7 +1897,7 @@ alexandrescu 关于 min max 的讨论:《再谈Min和Max》 ###7.3 Reactor与Linq ###7.4 Linq的C++实践 ###7.5 Boost.Hana -###7.5 更高更快更强:从Linq到FP +###7.6 更高更快更强:从Linq到FP ## 8 结语:讨论有益,争端无用 From 03ed334db01194f4eff0154549f373bd23c46861 Mon Sep 17 00:00:00 2001 From: wuye9036 Date: Fri, 18 Dec 2015 23:04:17 -0800 Subject: [PATCH 17/35] =?UTF-8?q?=E6=9B=B4=E6=96=B04=E7=AB=A0=E4=B9=8B?= =?UTF-8?q?=E5=90=8E=E7=9A=84=E5=86=99=E4=BD=9C=E8=AE=A1=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ReadMe.md | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index d050baa..0b2bc8e 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1876,30 +1876,35 @@ template class Y {}; // (4) error! ###4.2 将循环变成递归,将分支变成递归,将一切变成递归 ###4.3 实战单元:元编程的Fibonacci数列 -## 5 元编程下的算法 +## 5 元编程下的数据结构与算法 ###5.1 列表与数组 ###5.2 字典结构 ###5.3 “快速”排序 +###5.4 其它常用的“轮子” +boost.mpl -## 6 关于模板,你还需要知道的其它常识 -###6.1 类中类:灵活的模板定义 +## 6 模板的进阶技巧 +###6.1 嵌入类 ###6.2 Template-Template Class -###6.3 技巧单元:高阶函数——从函数到函数的组合 -###6.4 实战单元:STL中的Allocator Rebinder -###6.5 更好的编译器,更友善的出错信息 -###6.6 模板的症结:易于实现,难于完美 - -alexandrescu 关于 min max 的讨论:《再谈Min和Max》 +###6.3 高阶函数 +###6.4 闭包:模板的“基于对象” +stl allocator? +mpl::apply +###6.5 占位符(placeholder):在C++中实现方言的基石 +###6.6 编译期“多态” ## 7 模板的威力:从foreach, transform到Linq ###7.1 Foreach与Transform -###7.2 Reactor风格的编程 -###7.3 Reactor与Linq -###7.4 Linq的C++实践 -###7.5 Boost.Hana -###7.6 更高更快更强:从Linq到FP +###7.2 Boost中的模板 +Any Spirit Hana +###7.3 Reactor、Linq与C++中的实践 +###7.4 更高更快更强:从Linq到FP ## 8 结语:讨论有益,争端无用 +###8.1 更好的编译器,更友善的出错信息 +###8.2 模板的症结:易于实现,难于完美 + +alexandrescu 关于 min max 的讨论:《再谈Min和Max》 [1]: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf [2]: http://blog.llvm.org/2009/12/dreaded-two-phase-name-lookup.html From 11d56c7e85097f96289a72f06dbe22bb62dc8e9b Mon Sep 17 00:00:00 2001 From: wuye9036 Date: Sun, 20 Dec 2015 19:20:33 -0800 Subject: [PATCH 18/35] Update ReadMe.md --- ReadMe.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ReadMe.md b/ReadMe.md index 0b2bc8e..b18cbc1 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1905,7 +1905,10 @@ Any Spirit Hana ###8.2 模板的症结:易于实现,难于完美 alexandrescu 关于 min max 的讨论:《再谈Min和Max》 +std::experimental::any / boost.any 对于 reference 的处理 [1]: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf + [2]: http://blog.llvm.org/2009/12/dreaded-two-phase-name-lookup.html + [3]: https://goo.gl/zCRNYx From 68e384b817d1cb7073b2040c6e04478d5ad66ef9 Mon Sep 17 00:00:00 2001 From: wuye9036 Date: Sun, 20 Dec 2015 23:52:22 -0800 Subject: [PATCH 19/35] Update ReadMe.md --- ReadMe.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ReadMe.md b/ReadMe.md index b18cbc1..0bfcc5e 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1520,6 +1520,8 @@ error: variable has incomplete type 'A' 符合标准的写法需要将模板类的定义,和模板函数的定义分离开: +> TODO 此处例子不够恰当,并且描述有歧义。需要在未来版本中修订。 + ```C++ struct A; template struct X { From c7193eaf294f72dfbd76169d5dc5e2ca4fe347a9 Mon Sep 17 00:00:00 2001 From: soyli <529140640@qq.com> Date: Tue, 22 Dec 2015 14:24:34 +0800 Subject: [PATCH 20/35] =?UTF-8?q?=E9=94=99=E5=88=AB=E5=AD=97=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ReadMe.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ReadMe.md b/ReadMe.md index 0bfcc5e..1e7175c 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1621,7 +1621,7 @@ template struct X { ### 2.4 本章小结 -这一张是写作中最艰难的一章,中间停滞了将近一年。因为要说清楚C++模板中一些语法噪音和设计决议并不是一件轻松的事情。不过通过这一章的学习,我们知道了下面这几件事情: +这一章是写作中最艰难的一章,中间停滞了将近一年。因为要说清楚C++模板中一些语法噪音和设计决议并不是一件轻松的事情。不过通过这一章的学习,我们知道了下面这几件事情: 1. **部分特化/偏特化** 和 **特化** 相当于是模板实例化过程中的`if-then-else`。这使得我们根据不同类型,选择不同实现的需求得以实现; From e0a7eb81bf034019c7314e08129cdf2b1b9c49e8 Mon Sep 17 00:00:00 2001 From: wuye9036 Date: Wed, 23 Dec 2015 15:45:25 -0800 Subject: [PATCH 21/35] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E4=BA=86=E6=9F=90?= =?UTF-8?q?=E4=BA=9B=E8=A1=8C=E6=96=87=E9=97=AE=E9=A2=98=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ReadMe.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index 1e7175c..d5968db 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -53,6 +53,8 @@ C++编译器众多,且对模板的支持可能存在细微差别。如果没 * 需增加: * 模板的使用动机。 + * 增加“如何使用本文”一节。本节将说明全书的体例(强调字体、提示语、例子的组织),所有的描述、举例、引用在重审时将按照体例要求重新组织。 + * 除了用于描述语法的例子外,其他例子将尽量赋予实际意义,以方便阐述意图。 * 建议: * 比较模板和函数的差异性 * 蓝色:C++14 Return type deduction for normal functions 的分析 @@ -1690,7 +1692,7 @@ template class X {}; // ^^^^ 注意这里 ``` -对,就是这个``,跟在X后面的小尾巴,决定了第二条语句是第一条语句的跟班。所以,第二条语句,即“偏特化”,必须要符合原型X的基本形式,那就是只有一个参数。这也是为什么`DoWork`尝试以`template <> struct DoWork`的形式偏特化的时候,编译器会提示参数数量过多。 +对,就是这个``,跟在X后面的“小尾巴”,我们称作实参列表,决定了第二条语句是第一条语句的跟班。所以,第二条语句,即“偏特化”,必须要符合原型X的基本形式:那就是只有一个模板参数。这也是为什么`DoWork`尝试以`template <> struct DoWork`的形式偏特化的时候,编译器会提示模板实参数量过多。 另外一方面,在类模板的实例化阶段,它并不会直接去寻找 `template <> struct DoWork`这个小跟班,而是会先找到基本形式,`template struct DoWork;`,然后再去寻找相应的特化。 @@ -1699,8 +1701,8 @@ template class X {}; ```C++ template struct DoWork; // (0) 这是原型 -template <> struct DoWork {}; // (1) 这是 int 类型的"重载" -template <> struct DoWork {}; // (2) 这是 float 类型的"重载" +template <> struct DoWork {}; // (1) 这是 int 类型的特化 +template <> struct DoWork {}; // (2) 这是 float 类型的特化 DoWork i; // (3) ``` @@ -1716,7 +1718,7 @@ DoWork i; // (3) ```C++ template struct X ; // 0 // 原型有两个类型参数 - // 所以下面的这些偏特化的“小尾巴”(实参列表) + // 所以下面的这些偏特化的实参列表 // 也需要两个类型参数对应 template struct X {}; // 1 template struct X {}; // 2 @@ -1774,9 +1776,9 @@ template struct DoWork; 继而偏特化/特化问题也解决了: ```C++ -template <> struct DoWork {}; // (1) 这是 int 类型的"重载" -template <> struct DoWork {}; // (2) 这是 float 类型的"重载" -template <> struct DoWork {}; // (3) 这是 int, int 类型的“重载” +template <> struct DoWork {}; // (1) 这是 int 类型的特化 +template <> struct DoWork {}; // (2) 这是 float 类型的特化 +template <> struct DoWork {}; // (3) 这是 int, int 类型的特化 ``` 显而易见这个解决方案并不那么完美。首先,不管是偏特化还是用户实例化模板的时候,都需要多撰写好几个`void`,而且最长的那个参数越长,需要写的就越多;其次,如果我们的`DoWork`在程序维护的过程中新加入了一个参数列表更长的实例,那么最悲惨的事情就会发生 —— 原型、每一个偏特化、每一个实例化都要追加上`void`以凑齐新出现的实例所需要的参数数量。 From c038910b36941b4b55df4534193118a7c07302b7 Mon Sep 17 00:00:00 2001 From: wuye9036 Date: Wed, 23 Dec 2015 17:13:31 -0800 Subject: [PATCH 22/35] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BA=86=E9=83=A8?= =?UTF-8?q?=E5=88=86=E4=BD=93=E4=BE=8B=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ReadMe.md | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/ReadMe.md b/ReadMe.md index d5968db..10ae082 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -49,7 +49,28 @@ C++编译器众多,且对模板的支持可能存在细微差别。如果没 此外,部分复杂实例我们还在文中提供了在线的编译器预览以方便大家阅读和测试。在线编译器参见: http://gcc.godbolt.org/ 。 -###0.5 意见、建议、喷、补遗、写作计划 +###0.5 体例 + +####0.5.1 示例代码 + +```C++ +void SampleCode() { + // 这是一段示例代码 +} +``` + +####0.5.2 引用 + +引用自C++标准: + +> 1.1.2/1 这是一段引用或翻译自标准的文字 + +引用自其他图书: + +> 《书名》 +> 这是一段引用或翻译自其他图书的文字 + +###0.6 意见、建议、喷、补遗、写作计划 * 需增加: * 模板的使用动机。 @@ -1872,6 +1893,8 @@ template class Y {}; // (4) error! ####3.1.3 模板的默认实参 + + ###3.2 后悔药:SFINAE ###3.3 实战单元:获得类型的属性——类型萃取(Type Traits) @@ -1909,6 +1932,7 @@ Any Spirit Hana ###8.2 模板的症结:易于实现,难于完美 alexandrescu 关于 min max 的讨论:《再谈Min和Max》 + std::experimental::any / boost.any 对于 reference 的处理 [1]: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf From d897a5cba3c4ebc26544c601e61f22ec2af83692 Mon Sep 17 00:00:00 2001 From: wuye9036 Date: Wed, 23 Dec 2015 18:13:49 -0800 Subject: [PATCH 23/35] =?UTF-8?q?=E6=92=B0=E5=86=993.1.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ReadMe.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/ReadMe.md b/ReadMe.md index 10ae082..8f0125a 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1851,6 +1851,8 @@ void foo(){ ```C++ // Tuple 的声明,来自 boost +struct null_type; + 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, @@ -1893,6 +1895,24 @@ template class Y {}; // (4) error! ####3.1.3 模板的默认实参 +在上一节中,我们介绍了模板对默认实参的支持。当时我们的例子很简单,默认模板实参是一个确定的类型`void`或者自定义的`null_type`: + +```C++ +template < + typename T0, + typename T1 = void, + typename T2 = void +> class Tuple; +``` + +实际上,模板的默认参数不仅仅可以是一个确定的类型,它还能是以其他类型为参数的一个类型表达式。考虑下面的例子: + +```C++ +template struct IsFloat { + static bool const value = true; +} + +``` ###3.2 后悔药:SFINAE From 7224463f4fb35f44f3a2cd861986d2ab6fa08d04 Mon Sep 17 00:00:00 2001 From: wuye9036 Date: Sun, 3 Jan 2016 00:21:10 -0800 Subject: [PATCH 24/35] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=863.1.3=E7=9A=84?= =?UTF-8?q?=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ReadMe.md | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 72 insertions(+), 3 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index 8f0125a..682f739 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1905,15 +1905,84 @@ template < > class Tuple; ``` -实际上,模板的默认参数不仅仅可以是一个确定的类型,它还能是以其他类型为参数的一个类型表达式。考虑下面的例子: +实际上,模板的默认参数不仅仅可以是一个确定的类型,它还能是以其他类型为参数的一个类型表达式。 +考虑下面的例子:我们要执行两个同类型变量的除法,它对浮点、整数和其他类型分别采取不同的措施。 +对于浮点,执行内置除法;对于整数,要处理除零保护,防止引发异常;对于其他类型,执行一个叫做`CustomeDiv`的函数。 +第一步,我们先把浮点正确的写出来: ```C++ -template struct IsFloat { - static bool const value = true; +#include + +template T CustomDiv(T lhs, T rhs) { + // Custom Div的实现 } +template ::value> struct SafeDivide { + static T Do(T lhs, T rhs) { + return CustomDiv(lhs, rhs); + } +}; + +template struct SafeDivide{ // 偏特化A + static T Do(T lhs, T rhs){ + return lhs/rhs; + } +}; + +template struct SafeDivide{ // 偏特化B + static T Do(T lhs, T rhs){ + return lhs; + } +}; + +void foo(){ + SafeDivide::Do(1.0f, 2.0f); // 调用偏特化A + SafeDivide::Do(1, 2); // 调用偏特化B +} ``` +在实例化的时候,尽管我们只为`SafeDivide`指定了参数`T`,但是它的另一个参数`IsFloat`在缺省的情况下,可以根据`T`,求出表达式`std::is_floating_point::value`的值作为实参的值,带入到`SafeDivide`的匹配中。 + +嗯,这个时候我们要再把整型和其他类型纳入进来,无外乎就是加这么一个参数: + +```C++ +#include +#include + +template T CustomDiv(T lhs, T rhs) { + T v; + // Custom Div的实现 + return v; +} + +template < + typename T, + bool IsFloat = std::is_floating_point::value, + bool IsIntegral = std::is_integral::value +> struct SafeDivide { + static T Do(T lhs, T rhs) { + return CustomDiv(lhs, rhs); + } +}; + +template struct SafeDivide{ // 偏特化A + static T Do(T lhs, T rhs){ + return lhs/rhs; + } +}; + +template struct SafeDivide{ // 偏特化B + static T Do(T lhs, T rhs){ + return rhs == 0 ? 0 : lhs/rhs; + } +}; + +void foo(){ + SafeDivide::Do(1.0f, 2.0f); // 调用偏特化A + SafeDivide::Do(1, 2); // 调用偏特化B + SafeDivide>::Do({1.f, 2.f}, {1.f, -2.f}); // 调用一般形式 +} +``` ###3.2 后悔药:SFINAE ###3.3 实战单元:获得类型的属性——类型萃取(Type Traits) From 76c4f3f588604bb9eed43bcd38a89aa9cf34ada6 Mon Sep 17 00:00:00 2001 From: wuye9036 Date: Sun, 3 Jan 2016 00:24:21 -0800 Subject: [PATCH 25/35] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BA=86=E9=93=BE?= =?UTF-8?q?=E6=8E=A5=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ReadMe.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ReadMe.md b/ReadMe.md index 682f739..86d926f 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1943,7 +1943,7 @@ void foo(){ 在实例化的时候,尽管我们只为`SafeDivide`指定了参数`T`,但是它的另一个参数`IsFloat`在缺省的情况下,可以根据`T`,求出表达式`std::is_floating_point::value`的值作为实参的值,带入到`SafeDivide`的匹配中。 -嗯,这个时候我们要再把整型和其他类型纳入进来,无外乎就是加这么一个参数: +嗯,这个时候我们要再把整型和其他类型纳入进来,无外乎就是加这么一个参数(http://goo.gl/0Lqywt): ```C++ #include From ee89134cc681288633bdb44694f84dd7206d6e36 Mon Sep 17 00:00:00 2001 From: wuye9036 Date: Sun, 3 Jan 2016 00:25:05 -0800 Subject: [PATCH 26/35] Update ReadMe.md --- ReadMe.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ReadMe.md b/ReadMe.md index 86d926f..69fd316 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1943,7 +1943,7 @@ void foo(){ 在实例化的时候,尽管我们只为`SafeDivide`指定了参数`T`,但是它的另一个参数`IsFloat`在缺省的情况下,可以根据`T`,求出表达式`std::is_floating_point::value`的值作为实参的值,带入到`SafeDivide`的匹配中。 -嗯,这个时候我们要再把整型和其他类型纳入进来,无外乎就是加这么一个参数(http://goo.gl/0Lqywt): +嗯,这个时候我们要再把整型和其他类型纳入进来,无外乎就是加这么一个参数( http://goo.gl/0Lqywt ): ```C++ #include From 9302f34d9d9f125c16e212687ebd5842a7a6f5de Mon Sep 17 00:00:00 2001 From: wuye9036 Date: Sun, 3 Jan 2016 15:02:05 -0800 Subject: [PATCH 27/35] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BA=863.1.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ReadMe.md | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/ReadMe.md b/ReadMe.md index 69fd316..a9ba90e 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1752,6 +1752,7 @@ template struct X {}; // 7 template struct X, shared_ptr>; // 8 // 以下特化,分别对应哪个偏特化的实例? +// 此时偏特化中的T或U分别是什么类型? X v0; X v1; @@ -1764,7 +1765,7 @@ X v7; X v8; ``` -在上面这段例子中,有几个值得注意之处。首先,偏特化时的模板参数,和原型的模板参数没有任何关系,和原型不同,它的顺序完全不影响模式匹配的顺序,它只是偏特化模式,如``中`U`的声明,真正的模式,是由``体现出来的。 +在上面这段例子中,有几个值得注意之处。首先,偏特化时的模板参数,和原型的模板参数没有任何关系。和原型不同,它的顺序完全不影响模式匹配的顺序,它只是偏特化模式,如``中`U`的声明,真正的模式,是由``体现出来的。 这也是为什么在特化的时候,当所有类型都已经确定,我们就可以抛弃全部的模板参数,写出`template <> struct X`这样的形式:因为所有列表中所有参数都确定了,就不需要额外的形式参数了。 @@ -1984,6 +1985,63 @@ void foo(){ } ``` +当然,这时也许你会注意到,`is_integral`,`is_floating_point`和其他类类型三者是互斥的,那能不能只使用一个条件量来进行分派呢?答案当然是可以的( http://goo.gl/jYp5J2 ): + +```C++ +#include +#include + +template T CustomDiv(T lhs, T rhs) { + T v; + // Custom Div的实现 + return v; +} + +template struct SafeDivide { + static T Do(T lhs, T rhs) { + return CustomDiv(lhs, rhs); + } +}; + +template struct SafeDivide< + T, typename std::is_floating_point::type>{ // 偏特化A + static T Do(T lhs, T rhs){ + return lhs/rhs; + } +}; + +template struct SafeDivide< + T, typename std::is_integral::type>{ // 偏特化B + static T Do(T lhs, T rhs){ + return rhs == 0 ? 0 : lhs/rhs; + } +}; + +void foo(){ + SafeDivide::Do(1.0f, 2.0f); // 调用偏特化A + SafeDivide::Do(1, 2); // 调用偏特化B + SafeDivide>::Do({1.f, 2.f}, {1.f, -2.f}); +} +``` + +我们借助这个例子,帮助大家理解一下这个结构是怎么工作的: + +1. 对`SafeDivide` + + * 通过匹配类模板的泛化形式,计算默认实参,可以知道我们要匹配的模板实参是`SafeDivide` + + * 计算两个偏特化的形式的匹配:A得到``,和B得到 `` + + * 最后偏特化B的匹配结果和模板实参一致,使用它。 + +2. 针对`SafeDivide>` + + * 通过匹配类模板的泛化形式,可以知道我们要匹配的模板实参是`SafeDivide, true_type>` + + * 计算两个偏特化形式的匹配:A和B均得到`SafeDivide, false_type>` + + * A和B都与模板实参无法匹配,所以使用原型,调用`CustomDiv` + ###3.2 后悔药:SFINAE ###3.3 实战单元:获得类型的属性——类型萃取(Type Traits) From 4aa1ae1cd76541dacd58a8880b14d61753f30b48 Mon Sep 17 00:00:00 2001 From: wuye9036 Date: Mon, 4 Jan 2016 00:43:23 -0800 Subject: [PATCH 28/35] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BA=863.1.1=E7=A7=8D?= =?UTF-8?q?=E5=AF=B9=E6=A8=A1=E6=9D=BF=E5=81=8F=E7=89=B9=E5=8C=96=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E7=9A=84=E6=8F=8F=E8=BF=B0=E5=92=8C=E4=BE=8B=E5=AD=90?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ReadMe.md | 53 ++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index a9ba90e..16c125d 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1720,19 +1720,38 @@ template class X {}; 我们以`DoWork i;`为例,尝试复原一下编译器完成整个模板匹配过程的场景,帮助大家理解。看以下示例代码: ```C++ -template struct DoWork; // (0) 这是原型 +template struct DoWork; // (0) 这是原型 -template <> struct DoWork {}; // (1) 这是 int 类型的特化 -template <> struct DoWork {}; // (2) 这是 float 类型的特化 +template <> struct DoWork {}; // (1) 这是 int 类型的特化 +template <> struct DoWork {}; // (2) 这是 float 类型的特化 +template struct DoWork {}; // (3) 这是指针类型的偏特化 -DoWork i; // (3) +DoWork i; // (4) +DoWork pf; // (5) ``` -1. 编译器分析(0), (1), (2)三句,得知(0)是模板的原型,(1),(2)两句是模板(0)匹配的特例。我们假设有两个字典,第一个字典存储了模板原型,我们称之为`TemplateDict`。第二个字典`TemplateSpecDict`,存储了模板原型所对应的特化/偏特化形式。所以编译器在这三句时,可以视作`TemplateDict.add(DoWork)`,以及 `TemplateSpecDict.get(DoWork).add(int);` 和 `TemplateSpecDict.get(DoWork).add(float);` +1. 编译器分析(0), (1), (2)三句,得知(0)是模板的原型,(1),(2),(3)是模板(0)的特化或偏特化。我们假设有两个字典,第一个字典存储了模板原型,我们称之为`TemplateDict`。第二个字典`TemplateSpecDict`,存储了模板原型所对应的特化/偏特化形式。所以编译器在这几句时,可以视作 -2. (3) 试图以`int`实例化类模板`DoWork`。它会在`TemplateDict`中,找到`DoWork`,它有一个形式参数`T`接受类型,正好和我们实例化的要求相符合。并且此时`T`被推导为`int`。 +```C++ +// 以下为伪代码 +TemplateDict[DoWork] = { + DoWork, + DoWork, + DoWork +}; +``` -3. 编译器这个时候就想了,那它会不会有针对`int`的特化呢?于是就去`TemplateSpecDict`中查找,发现果然有`DoWork`的存在,就使用了这个特例。 +2. (4) 试图以`int`实例化类模板`DoWork`。它会在`TemplateDict`中,找到`DoWork`,它有一个形式参数`T`接受类型,正好和我们实例化的要求相符合。并且此时`T`被推导为`int`。(5) 中的`float*`也是同理。 + +```C++ +// 以下为 DoWork 查找对应匹配的伪代码 +templateProtoInt = TemplateDict.find(DoWork, int); // 查找模板原型,查找到(0) +template = templatePrototype.match(int); // 以 int 对应 int 匹配到 (1) + +// 以下为DoWork 查找对应匹配的伪代码 +templateProtoIntPtr = TemplateDict.find(DoWork, float*) // 查找模板原型,查找到(0) +template = templateProtoIntPtr.match(float*) // 以 float* 对应 U* 匹配到 (3),此时U为float +``` 那么根据上面的步骤所展现的基本原理,我们随便来几个练习: @@ -1765,11 +1784,27 @@ X v7; X v8; ``` -在上面这段例子中,有几个值得注意之处。首先,偏特化时的模板参数,和原型的模板参数没有任何关系。和原型不同,它的顺序完全不影响模式匹配的顺序,它只是偏特化模式,如``中`U`的声明,真正的模式,是由``体现出来的。 +在上面这段例子中,有几个值得注意之处。首先,偏特化时的模板形参,和原型的模板形参没有任何关系。和原型不同,它的顺序完全不影响模式匹配的顺序,它只是偏特化模式,如``中`U`的声明,真正的模式,是由``体现出来的。 这也是为什么在特化的时候,当所有类型都已经确定,我们就可以抛弃全部的模板参数,写出`template <> struct X`这样的形式:因为所有列表中所有参数都确定了,就不需要额外的形式参数了。 -其次,作为一个模式匹配,偏特化的实参列表中展现出来的“样子”,就是它能被匹配的原因。比如,`struct X`中,要求模板的两个参数必须是相同的类型。而`struct X`,则代表第二个模板类型参数必须是第一个模板类型参数的指针,比如`X`就能匹配上。当然,除了简单的指针、`const`和`volatile`修饰符,其他的类模板也可以作为偏特化时的“模式”出现,例如示例8,它要求传入同一个类型的`unique_ptr`和`shared_ptr`。 +其次,作为一个模式匹配,偏特化的实参列表中展现出来的“样子”,就是它能被匹配的原因。比如,`struct X`中,要求模板的两个参数必须是相同的类型。而`struct X`,则代表第二个模板类型参数必须是第一个模板类型参数的指针,比如`X`就能匹配上。当然,除了简单的指针、`const`和`volatile`修饰符,其他的类模板也可以作为偏特化时的“模式”出现,例如示例8,它要求传入同一个类型的`unique_ptr`和`shared_ptr`。C++标准中指出下列模式都是可以被匹配的: + +> N3337, 14.8.2.5/8 + +> 令`T`是模板类型实参或者类型列表(如 _int, float, double_ 这样的,`TT`是template-template实参(参见6.2节),`i`是模板的非类型参数(整数、指针等),则以下形式的形参都会参与匹配: + +> `T`,`cv-list T`,`T*`, `_template-name_ `, `T&`, `T&&` + +>`T [ _integer-constant_ ]` + +>`_type_ (T)`, `T()`, `T(T)` + +>`T _type_ ::*`, `_type_ T::*`, `T T::*` + +>`T (_type_ ::*)()`, `_type_ (T::*)()`, `_type_ (_type_ ::*)(T)`, `_type_ (T::*)(T)`, `T (_type_ ::*)(T)`, `T (T::*)()`, `T (T::*)(T)` + +>`_type_ [i]`, `_template-name_ `, `TT`, `TT`, `TT<>` 对于某些实例化,偏特化的选择并不是唯一的。比如v4的参数是``,能够匹配的就有三条规则,1,6和7。很显然,6还是比7好一些,因为能多匹配一个指针。但是1和6,就很难说清楚谁更好了。一个说明了两者类型相同;另外一个则说明了两者都是指针。所以在这里,编译器也没办法决定使用那个,只好爆出了编译器错误。 From 4b46fcd18b987238e13e1de079374655598ec601 Mon Sep 17 00:00:00 2001 From: wuye9036 Date: Mon, 4 Jan 2016 01:05:27 -0800 Subject: [PATCH 29/35] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BA=86=E9=83=A8?= =?UTF-8?q?=E5=88=86=E4=BB=A3=E7=A0=81=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ReadMe.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index 16c125d..c03ec9a 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1842,7 +1842,7 @@ template <> struct DoWork {}; // (3) 这是 int, int 类型的特 所幸模板参数也有一个和函数参数相同的特性:默认实参(Default Arguments)。只需要一个例子,你们就能看明白了(http://goo.gl/TtmcY9): -```C++ +``` C++ template struct DoWork; template struct DoWork {}; @@ -2022,7 +2022,7 @@ void foo(){ 当然,这时也许你会注意到,`is_integral`,`is_floating_point`和其他类类型三者是互斥的,那能不能只使用一个条件量来进行分派呢?答案当然是可以的( http://goo.gl/jYp5J2 ): -```C++ +``` C++ #include #include From c0dc52e4568b683c98f6176e57df15a36f2054a9 Mon Sep 17 00:00:00 2001 From: wuye9036 Date: Mon, 4 Jan 2016 01:08:55 -0800 Subject: [PATCH 30/35] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E9=83=A8=E5=88=86?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E7=A4=BA=E4=BE=8B=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ReadMe.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ReadMe.md b/ReadMe.md index c03ec9a..61fd210 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1946,6 +1946,7 @@ template < 对于浮点,执行内置除法;对于整数,要处理除零保护,防止引发异常;对于其他类型,执行一个叫做`CustomeDiv`的函数。 第一步,我们先把浮点正确的写出来: + ```C++ #include @@ -2022,7 +2023,7 @@ void foo(){ 当然,这时也许你会注意到,`is_integral`,`is_floating_point`和其他类类型三者是互斥的,那能不能只使用一个条件量来进行分派呢?答案当然是可以的( http://goo.gl/jYp5J2 ): -``` C++ +```cpp #include #include From 02687cfcf3a076e177d1c8882cf3dd21e7c07530 Mon Sep 17 00:00:00 2001 From: wuye9036 Date: Mon, 4 Jan 2016 15:23:32 -0800 Subject: [PATCH 31/35] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E4=BA=863.1.1=E4=B8=AD?= =?UTF-8?q?=E9=83=A8=E5=88=86=E8=A1=8C=E6=96=87=E9=97=AE=E9=A2=98=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ReadMe.md | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index 61fd210..d86c17d 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1730,7 +1730,7 @@ DoWork i; // (4) DoWork pf; // (5) ``` -1. 编译器分析(0), (1), (2)三句,得知(0)是模板的原型,(1),(2),(3)是模板(0)的特化或偏特化。我们假设有两个字典,第一个字典存储了模板原型,我们称之为`TemplateDict`。第二个字典`TemplateSpecDict`,存储了模板原型所对应的特化/偏特化形式。所以编译器在这几句时,可以视作 +首先,编译器分析(0), (1), (2)三句,得知(0)是模板的原型,(1),(2),(3)是模板(0)的特化或偏特化。我们假设有两个字典,第一个字典存储了模板原型,我们称之为`TemplateDict`。第二个字典`TemplateSpecDict`,存储了模板原型所对应的特化/偏特化形式。所以编译器在这几句时,可以视作 ```C++ // 以下为伪代码 @@ -1741,16 +1741,18 @@ TemplateDict[DoWork] = { }; ``` -2. (4) 试图以`int`实例化类模板`DoWork`。它会在`TemplateDict`中,找到`DoWork`,它有一个形式参数`T`接受类型,正好和我们实例化的要求相符合。并且此时`T`被推导为`int`。(5) 中的`float*`也是同理。 +然后 (4) 试图以`int`实例化类模板`DoWork`。它会在`TemplateDict`中,找到`DoWork`,它有一个形式参数`T`接受类型,正好和我们实例化的要求相符合。并且此时`T`被推导为`int`。(5) 中的`float*`也是同理。 ```C++ -// 以下为 DoWork 查找对应匹配的伪代码 -templateProtoInt = TemplateDict.find(DoWork, int); // 查找模板原型,查找到(0) -template = templatePrototype.match(int); // 以 int 对应 int 匹配到 (1) +{ // 以下为 DoWork 查找对应匹配的伪代码 + templateProtoInt = TemplateDict.find(DoWork, int); // 查找模板原型,查找到(0) + template = templatePrototype.match(int); // 以 int 对应 int 匹配到 (1) +} -// 以下为DoWork 查找对应匹配的伪代码 -templateProtoIntPtr = TemplateDict.find(DoWork, float*) // 查找模板原型,查找到(0) -template = templateProtoIntPtr.match(float*) // 以 float* 对应 U* 匹配到 (3),此时U为float +{ // 以下为DoWork 查找对应匹配的伪代码 + templateProtoIntPtr = TemplateDict.find(DoWork, float*) // 查找模板原型,查找到(0) + template = templateProtoIntPtr.match(float*) // 以 float* 对应 U* 匹配到 (3),此时U为float +} ``` 那么根据上面的步骤所展现的基本原理,我们随便来几个练习: @@ -1794,17 +1796,17 @@ X v8; > 令`T`是模板类型实参或者类型列表(如 _int, float, double_ 这样的,`TT`是template-template实参(参见6.2节),`i`是模板的非类型参数(整数、指针等),则以下形式的形参都会参与匹配: -> `T`,`cv-list T`,`T*`, `_template-name_ `, `T&`, `T&&` +> `T`,`cv-list T`,`T*`, `template-name `, `T&`, `T&&` ->`T [ _integer-constant_ ]` +>`T [ integer-constant ]` ->`_type_ (T)`, `T()`, `T(T)` +>`type (T)`, `T()`, `T(T)` ->`T _type_ ::*`, `_type_ T::*`, `T T::*` +>`T type ::*`, `type T::*`, `T T::*` ->`T (_type_ ::*)()`, `_type_ (T::*)()`, `_type_ (_type_ ::*)(T)`, `_type_ (T::*)(T)`, `T (_type_ ::*)(T)`, `T (T::*)()`, `T (T::*)(T)` +>`T (type ::*)()`, `type (T::*)()`, `type (type ::*)(T)`, `type (T::*)(T)`, `T (type ::*)(T)`, `T (T::*)()`, `T (T::*)(T)` ->`_type_ [i]`, `_template-name_ `, `TT`, `TT`, `TT<>` +>`type [i]`, `template-name `, `TT`, `TT`, `TT<>` 对于某些实例化,偏特化的选择并不是唯一的。比如v4的参数是``,能够匹配的就有三条规则,1,6和7。很显然,6还是比7好一些,因为能多匹配一个指针。但是1和6,就很难说清楚谁更好了。一个说明了两者类型相同;另外一个则说明了两者都是指针。所以在这里,编译器也没办法决定使用那个,只好爆出了编译器错误。 @@ -1935,9 +1937,7 @@ template class Y {}; // (4) error! ```C++ template < - typename T0, - typename T1 = void, - typename T2 = void + typename T0, typename T1 = void, typename T2 = void > class Tuple; ``` From 250c73b42a592b76a7e44aca306d373ea0b50fcb Mon Sep 17 00:00:00 2001 From: wuye9036 Date: Thu, 9 Jun 2016 20:40:28 -0700 Subject: [PATCH 32/35] Update ReadMe.md Added section 'SFINAE' --- ReadMe.md | 460 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 454 insertions(+), 6 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index d86c17d..4f8b684 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -2079,7 +2079,454 @@ void foo(){ * A和B都与模板实参无法匹配,所以使用原型,调用`CustomDiv` ###3.2 后悔药:SFINAE -###3.3 实战单元:获得类型的属性——类型萃取(Type Traits) + +考虑下面这个函数模板: + +``` C++ +template +void foo(T t, typename U::type u) { + // ... +} +``` + +到本节为止,我们所有的例子都保证了一旦咱们敲定了模板参数中 `T` 和 `U`,函数参变量 `t` 和 `u` 的类型都是成立的,比如下面这样: + +``` C++ +struct X { + typedef float type; +}; + +template +void foo(T t, typename U::type u) { + // ... +} + +void callFoo() { + foo(5, 5.0); // T == int, typename U::type == X::type == int +} +``` + +那么这里有一个可能都不算是问题的问题 —— 对于下面的代码,你认为它会提示怎么样的错误: + +```C++ +struct X { + typedef float type; +}; + +struct Y { + typedef float type2; +}; + +template +void foo(T t, typename U::type u) { + // ... +} + +void callFoo() { + foo(5, 5.0); // T == int, typename U::type == X::type == int + foo(5, 5.0); // ??? +} +``` + +这个时候你也许会说:啊,这个简单,`Y` 没有 `type` 这个成员自然会出错啦!嗯,这个时候咱们来看看Clang给出的结果: + +``` +error: no matching function for call to 'foo' + foo(5, 5.0); // ??? + ^~~~~~~~~~~ + note: candidate template ignored: substitution failure [with T = int, U = Y]: no type named 'type' in 'Y' + void foo(T t, typename U::type u) { +``` + +完整翻译过来就是,直接的出错原因是没有匹配的 `foo` 函数,间接原因是尝试用 `[T = int, U = y]` 做类型替换的时候失败了,所以这个函数模板就被忽略了。等等,不是出错,而是被忽略了?那么也就是说,只要有别的能匹配的类型兜着,编译器就无视这里的失败了? + +银河火箭队的阿喵说,就是这样。不信邪的朋友可以试试下面的代码: + +```C++ +struct X { + typedef float type; +}; + +struct Y { + typedef float type2; +}; + +template +void foo(T t, typename U::type u) { + // ... +} + +template +void foo(T t, typename U::type2 u) { + // ... +} +void callFoo() { + foo(5, 5.0); // T == int, typename U::type == X::type == int + foo( 1, 1.0 ); // ??? +} +``` + +这下相信编译器真的是不关心替换失败了吧。我们管这种只要有正确的候选,就无视替换失败的做法为SFINAE。 + +我们不用纠结这个词的发音,它来自于 Substitution failure is not an error 的首字母缩写。这一句之乎者也般难懂的话,由之乎者 —— 啊,不,Substitution,Failure和Error三个词构成。 + +我们从最简单的词“Error”开始理解。Error就是一般意义上的编译错误。一旦出现编译错误,大家都知道,编译器就会中止编译,并且停止接下来的代码生成和链接等后续活动。 + +其次,我们再说“Failure”。很多时候光看字面意思,很多人会把 Failure 和 Error 等同起来。但是实际上Failure很多场合下只是一个中性词。比如我们看下面这个虚构的例子就知道这两者的区别了。 + +假设我们有一个语法分析器,其中某一个规则需要匹配一个token,它可以是标识符,字面量或者是字符串,那么我们会有下面的代码: + +```C++ +switch(token) +{ +case IDENTIFIER: + // do something + break; +case LITERAL_NUMBER: + // do something + break; +case LITERAL_STRING: + // do something + break; +default: + throw WrongToken(token); +} +``` +假如我们当前的token是 `LITERAL_STRING` 的时候,那么第一步它在匹配 `IDENTIFIER` 时,我们可以认为它失败(failure)了,但是它在第三步就会匹配上,所以它并不是一个错误。 + +但是如果这个token既不是标识符、也不是数字字面量、也不是字符串字面量,而且我们的语法规定除了这三类值以外其他统统都是非法的时,我们才认为它是一个error。 + +大家所熟知的函数重载也是如此。比如说下面这个例子: + +```C++ +struct A {}; +struct B: public A {}; +struct C {}; + +void foo(A const&) {} +void foo(B const&) {} + +void callFoo() { + foo( A() ); + foo( B() ); + foo( C() ); +} +``` + +那么 `foo( A() )` 虽然匹配 `foo(B const&)` 会失败,但是它起码能匹配 `foo(A const&)`,所以它是正确的;`foo( B() )` 能同时匹配两个函数原型,但是 `foo(B const&)` 要更好一些,因此它选择了这个原型。而 `foo( C() );` 因为两个函数都匹配失败(Failure)了,所以它找不到相应的原型,这时才会爆出一个编译器错误(Error)。 + +所以到这里我们就明白了,在很多情况下,Failure is not an error。编译器在遇到Failure的时候,往往还需要尝试其他的可能性。 + +好,现在我们把最后一个词,Substitution,加入到我们的字典中。现在这句话的意思就是说,我们要把 Failure is not an error 的概念,推广到Substitution阶段。 + +所谓substitution,就是将函数模板中的形参,替换成实参的过程。概念很简洁但是实现却颇多细节,所以C++标准中对这一概念的解释比较拗口。它分别指出了以下几点: + + * 什么时候函数模板会发生实参 替代(Substitute) 形参的行为; + + * 什么样的行为被称作 Substitution; + + * 什么样的行为不可以被称作 Substitution Failure —— 他们叫SFINAE error。 + +我们在此不再详述,有兴趣的同学可以参照 http://en.cppreference.com/w/cpp/language/sfinae ,这是标准的一个精炼版本。这里我们简单的解释一下。 + +考虑我们有这么个函数签名: + +```C++ +template < + typename T0, + // 一大坨其他模板参数 + typename U = /* 和前面T有关的一大坨 */ +> +RType /* 和模板参数有关的一大坨 */ +functionName ( + PType0 /* PType0 是和模板参数有关的一大坨 */, + PType1 /* PType1 是和模板参数有关的一大坨 */, + // ... 其他参数 +) { + // 实现,和模板参数有关的一大坨 +} +``` + +那么,在这个函数模板被实例化的时候,所有函数签名上的“和模板参数有关的一大坨”被推导出具体类型的过程,就是替换。一个更具体的例子来解释上面的“一大坨”: + +```C++ +template < + typename T, + typenname U = typename vector::iterator // 1 +> +typename vector::value_type // 1 + foo( + T*, // 1 + T&, // 1 + typename T::internal_type, // 1 + typename add_reference::type, // 1 + int // 这里都不需要 substitution + ) +{ + // 整个实现部分,都没有 substitution。这个很关键。 +} +``` + +所有标记为 `1` 的部分,都是需要替换的部分,而它们在替换过程中的失败(failure),就称之为替换失败(substitution failure)。 + +下面的代码是提供了一些替换成功和替换失败的示例: + +```C++ +struct X { + typedef int type; +}; + +struct Y { + typedef int type2; +}; + +template void foo(typename T::type); // Foo0 +template void foo(typename T::type2); // Foo1 +template void foo(T); // Foo2 + +void callFoo() { + foo(5); // Foo0: Succeed, Foo1: Failed, Foo2: Failed + foo(10); // Foo0: Failed, Foo1: Succeed, Foo2: Failed + foo(15); // Foo0: Failed, Foo1: Failed, Foo2: Succeed +} +``` + +在这个例子中,当我们指定 `foo` 的时候,substitution就开始工作了,而且会同时工作在三个不同的 `foo` 签名上。如果我们仅仅因为 `Y` 没有 `type`,匹配 `Foo0` 失败了,就宣布代码有错,中止编译,那显然是武断的。因为 `Foo1` 是可以被正确替换的,我们也希望 `Foo1` 成为 `foo` 的原型。 + +std/boost库中的 `enable_if` 是 SFINAE 最直接也是最主要的应用。所以我们通过下面 `enable_if` 的例子,来深入理解一下 SFINAE 在模板编程中的作用。 + +假设我们有两个不同类型的计数器(counter),一种是普通的整数类型,另外一种是一个复杂对象,它从接口 `ICounter` 继承,这个接口有一个成员叫做increase实现计数功能。现在,我们想把这两种类型的counter封装一个统一的调用:inc_counter。那么,我们直觉会简单粗暴的写出下面的代码: + +```C++ +struct ICounter { + virtual void increase() = 0; + virtual ~ICounter() {} +}; + +struct Counter: public ICounter { + void increase() override { + // Implements + } +}; + +template +void inc_counter(T& counterObj) { + counterObj.increase(); +} + +template +void inc_counter(T& intTypeCounter){ + ++intTypeCounter; +} + +void doSomething() { + Counter cntObj; + uint32_t cntUI32; + + // blah blah blah + inc_counter(cntObj); + inc_counter(cntUI32); +} +``` + +我们非常希望它展现出预期的行为。因为其实我们是知道对于任何一个调用,两个 `inc_counter` 只有一个是能够编译正确的。“有且唯一”,我们理应当期望编译器能够挑出那个唯一来。 + +可惜编译器做不到这一点。首先,它就告诉我们,这两个签名 + +```C++ +template void inc_counter(T& counterObj); +template void inc_counter(T& intTypeCounter); +``` + +其实是一模一样的。我们遇到了 `redefinition`。 + +我们看看 `enable_if` 是怎么解决这个问题的。我们通过 `enable_if` 这个 `T` 对于不同的实例做个限定: + +```C++ +template void inc_counter( + T& counterObj, + typename std::enable_if< + is_base_of::value + >::type* = nullptr ); + +template void inc_counter( + T& counterInt, + typename std::enable_if< + std::is_integral::value + >::type* = nullptr ); +``` + +然后我们解释一下,这个 `enable_if` 是怎么工作的,语法为什么这么丑: + +首先,替换(substitution)只有在推断函数类型的时候,才会起作用。推断函数类型需要参数的类型,所以, `typename std::enable_if::value>::type` 这么一长串代码,就是为了让 `enable_if` 参与到函数类型中; + +其次, `is_integral::value` 返回一个布尔类型的编译器常数,告诉我们它是或者不是一个 `integral type`,`enable_if` 的作用就是,如果这个 `C` 值为 `True`,那么 `enable_if::type` 就会被推断成一个 `void` 或者是别的什么类型,让整个函数匹配后的类型变成 `void inc_counter(int & counterInt, void* dummy = nullptr);` 如果这个值为 `False` ,那么 `enable_if` 这个特化形式中,压根就没有这个 `::type`,于是替换就失败了。和我们之前的例子中一样,这个函数原型就不会被产生出来。 + +所以我们能保证,无论对于 `int` 还是 `counter` 类型的实例,我们都只有一个函数原型通过了substitution —— 这样就保证了它的“有且唯一”,编译器也不会因为你某个替换失败而无视成功的那个实例。 + +这个例子说到了这里,熟悉C++的你,一定会站出来说我们只要把第一个签名改成: + +```C++ +void inc_counter(ICounter& counterObj); +``` + +就能完美解决这个问题了,根本不需要这么复杂的编译器机制。 + +嗯,你说的没错,在这里这个特性一点都没用。 + +这也提醒我们,当你觉得需要写 `enable_if` 的时候,首先要考虑到以下可能性: + + * 重载(对模板函数) + + * 偏特化(对模板类而言) + + * 虚函数 + + +但是问题到了这里并没有结束。因为 `increase` 毕竟是个虚函数。假如 `Counter` 需要调用的地方实在是太多了,这个时候我们会非常期望 `increase` 不再是个虚函数以提高性能。此时我们会调整继承层级: + +```C++ +struct ICounter {}; +struct Counter: public ICounter { + void increase() { + // impl + } +}; +``` + +那么原有的 `void inc_counter(ICounter& counterObj)` 就无法再执行下去了。这个时候你可能会考虑一些变通的办法: + +```C++ +template +void inc_counter(ICounter& c) {}; + +template +void inc_counter(T& c) { ++c; }; + +void doSomething() { + Counter cntObj; + uint32_t cntUI32; + + // blah blah blah + inc_counter(cntObj); // 1 + inc_counter(static_cast(cntObj)); // 2 + inc_counter(cntUI32); // 3 +} +``` + +对于调用 `1`,因为 `cntObj` 到 `ICounter` 是需要类型转换的,所以比 `void inc_counter(T&) [T = Counter]` 要更差一些。然后它会直接实例化后者,结果实现变成了 `++cntObj`,BOOM! + +那么我们做 `2` 试试看?嗯,工作的很好。但是等等,我们的初衷是什么来着?不就是让 `inc_counter` 对不同的计数器类型透明吗?这不是又一夜回到解放前了? + +所以这个时候,就能看到 `enable_if` 是如何通过 SFINAE 发挥威力的了: + +```C++ +#include +#include +#include + +struct ICounter {}; +struct Counter: public ICounter { + void increase() { + // impl + } +}; + +template void inc_counter( + T& counterObj, + typename std::enable_if< + std::is_base_of::value + >::type* = nullptr ){ + counterObj.increase(); +} + +template void inc_counter( + T& counterInt, + typename std::enable_if< + std::is_integral::value + >::type* = nullptr ){ + ++counterInt; +} + +void doSomething() { + Counter cntObj; + uint32_t cntUI32; + + // blah blah blah + inc_counter(cntObj); // OK! + inc_counter(cntUI32); // OK! +} +``` + +这个代码是不是看起来有点脏脏的。眼尖的你定睛一瞧,咦, `ICounter` 不是已经空了吗,为什么我们还要用它作为基类呢? + +这是个好问题。在本例中,我们用它来区分一个`counter`是不是继承自`ICounter`。最终目的,是希望知道 `counter` 有没有 `increase` 这个函数。 + +所以 `ICounter` 只是相当于一个标签。而于情于理这个标签都是个累赘。但是在C++11之前,我们并没有办法去写类似于: + +```C++ +template void foo(T& c, decltype(c.increase())* = nullptr); +``` + +这样的函数签名,因为假如 `T` 是 `int`,那么 `c.increase()` 这个函数调用就不存在。但它又不属于Type Failure,而是一个Expression Failure,在C++11之前它会直接导致编译器出错,这并不是我们所期望的。所以我们才退而求其次,用一个类似于标签的形式来提供我们所需要的类型信息。以后的章节,后面我们会说到,这种和类型有关的信息我们可以称之为 `type traits`。 + +到了C++11,它正式提供了 Expression SFINAE,这时我们就能抛开 `ICounter` 这个无用的Tag,直接写出我们要写的东西: + +```C++ +struct Counter { + void increase() { + // Implements + } +}; + +template +void inc_counter(T& intTypeCounter, std::decay_t* = nullptr) { + ++intTypeCounter; +} + +template +void inc_counter(T& counterObj, std::decay_t* = nullptr) { + counterObj.increase(); +} + +void doSomething() { + Counter cntObj; + uint32_t cntUI32; + + // blah blah blah + inc_counter(cntObj); + inc_counter(cntUI32); +} +``` + +此外,还有一种情况只能使用 SFINAE,而无法使用包括继承、重载在内的任何方法,这就是Universal Reference。比如, + +```C++ +// 这里的a是个通用引用,可以准确的处理左右值引用的问题。 +template void foo(ArgT&& a); +``` + +加入我们要限定ArgT只能是 float 的衍生类型,那么写成下面这个样子是不对的,它实际上只能接受 float 的右值引用。 + +```C++ +void foo(float&& a); +``` + +此时的唯一选择,就是使用Universal Reference,并增加 `enable_if` 限定类型,如下面这样: + +```C++ +template +void foo( + ArgT&& a, + typename std::enabled_if< + is_same::value + >::type* = nullptr +); +``` + +从上面这些例子可以看到,SFINAE最主要的作用,是保证编译器在泛型函数、偏特化、及一般重载函数中遴选函数原型的候选列表时不被打断。除此之外,它还有一个很重要的元编程作用就是实现部分的编译期自省和反射。 + +虽然它写起来并不直观,但是对于既没有编译器自省、也没有Concept的C++1y来说,已经是最好的选择了。 ## 4 用模板写程序吧!骚年! ###4.1 模板上的递归 @@ -2087,11 +2534,12 @@ void foo(){ ###4.3 实战单元:元编程的Fibonacci数列 ## 5 元编程下的数据结构与算法 -###5.1 列表与数组 -###5.2 字典结构 -###5.3 “快速”排序 -###5.4 其它常用的“轮子” -boost.mpl +###5.1 获得类型的属性——类型萃取(Type Traits) +###5.2 列表与数组 +###5.3 字典结构 +###5.4 “快速”排序 +###5.5 其它常用的“轮子” +boost.hana ## 6 模板的进阶技巧 ###6.1 嵌入类 From 45c001f8a3810948c5690759e26a91e465be8b7c Mon Sep 17 00:00:00 2001 From: wuye9036 Date: Tue, 14 Jun 2016 12:19:26 -0700 Subject: [PATCH 33/35] Fixed #14. --- ReadMe.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index 4f8b684..9a5aded 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1469,8 +1469,8 @@ template struct Y }; void poo(){ - X::foo(); - X::foo(); + Y::foo(); + Y::foo(); } ``` From c4f8640f632c00703161686cd735a8a3461313ed Mon Sep 17 00:00:00 2001 From: wuye9036 Date: Tue, 14 Jun 2016 12:22:43 -0700 Subject: [PATCH 34/35] Fixed #19 --- ReadMe.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index 9a5aded..14d0e7f 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -729,7 +729,7 @@ Variant addFloatOrMulInt(Variant const* a, Variant const* b) 更常见的是 `void*`: ``` C++ -#define BIN_OP(type, a, op, b, result) (*(type const *)(result)) = (*(type const *)(a)) op (*(type const*)(b)) +#define BIN_OP(type, a, op, b, result) (*(type *)(result)) = (*(type const *)(a)) op (*(type const*)(b)) void doDiv(void* out, void const* data0, void const* data1, DATA_TYPE type) { if(type == TYPE_INT) @@ -1030,9 +1030,9 @@ void PrintID() // TypeToID 的模板“原型” // ... -template class TypeToID +template <> class TypeToID { - static int const ID = 0xF10A7; + static int const ID = 0xF10A7; }; ``` @@ -1041,12 +1041,12 @@ template class TypeToID ``` C++ template <> class TypeToID { - static int const ID = 0x401d; + static int const ID = 0x401d; }; void PrintID() { - cout << "ID of uint8_t: " << TypeToID::ID << endl; + cout << "ID of uint8_t: " << TypeToID::ID << endl; } ``` From ea1e6ef7675ee573ffa47d50c645b752b8a093fe Mon Sep 17 00:00:00 2001 From: wuye9036 Date: Tue, 14 Jun 2016 12:27:57 -0700 Subject: [PATCH 35/35] =?UTF-8?q?=E8=B0=83=E6=95=B4=E4=BA=86=E7=AB=A0?= =?UTF-8?q?=E8=8A=82=E5=AE=89=E6=8E=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ReadMe.md | 50 ++++++++++++++++++++++---------------------------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index 14d0e7f..0890ab9 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -2528,42 +2528,36 @@ void foo( 虽然它写起来并不直观,但是对于既没有编译器自省、也没有Concept的C++1y来说,已经是最好的选择了。 -## 4 用模板写程序吧!骚年! -###4.1 模板上的递归 -###4.2 将循环变成递归,将分支变成递归,将一切变成递归 -###4.3 实战单元:元编程的Fibonacci数列 - -## 5 元编程下的数据结构与算法 -###5.1 获得类型的属性——类型萃取(Type Traits) -###5.2 列表与数组 -###5.3 字典结构 -###5.4 “快速”排序 -###5.5 其它常用的“轮子” +## 4 元编程下的数据结构与算法 +###4.1 表达式与数值计算 +###4.1 获得类型的属性——类型萃取(Type Traits) +###4.2 列表与数组 +###4.3 字典结构 +###4.4 “快速”排序 +###4.5 其它常用的“轮子” boost.hana -## 6 模板的进阶技巧 -###6.1 嵌入类 -###6.2 Template-Template Class -###6.3 高阶函数 -###6.4 闭包:模板的“基于对象” +## 5 模板的进阶技巧 +###5.1 嵌入类 +###5.2 Template-Template Class +###5.3 高阶函数 +###5.4 闭包:模板的“基于对象” stl allocator? mpl::apply -###6.5 占位符(placeholder):在C++中实现方言的基石 -###6.6 编译期“多态” +###5.5 占位符(placeholder):在C++中实现方言的基石 +###5.6 编译期“多态” -## 7 模板的威力:从foreach, transform到Linq -###7.1 Foreach与Transform -###7.2 Boost中的模板 +## 6 模板的威力:从foreach, transform到Linq +###6.1 Foreach与Transform +###6.2 Boost中的模板 Any Spirit Hana -###7.3 Reactor、Linq与C++中的实践 -###7.4 更高更快更强:从Linq到FP - -## 8 结语:讨论有益,争端无用 -###8.1 更好的编译器,更友善的出错信息 -###8.2 模板的症结:易于实现,难于完美 +###6.3 Reactor、Linq与C++中的实践 +###6.4 更高更快更强:从Linq到FP +## 7 结语:讨论有益,争端无用 +###7.1 更好的编译器,更友善的出错信息 +###7.2 模板的症结:易于实现,难于完美 alexandrescu 关于 min max 的讨论:《再谈Min和Max》 - std::experimental::any / boost.any 对于 reference 的处理 [1]: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf