From 7819d831fdc4dac3a2b8b486ddec1d58e1956593 Mon Sep 17 00:00:00 2001 From: Ye WU Date: Wed, 2 Dec 2015 16:22:16 -0800 Subject: [PATCH] =?UTF-8?q?=E7=AC=AC=E4=BA=8C=E7=AB=A0=E7=BB=93=E6=9D=9F?= =?UTF-8?q?=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