diff --git a/CppTemplateTutorial.cpp b/CppTemplateTutorial.cpp index c836c94..f5c0e46 100644 --- a/CppTemplateTutorial.cpp +++ b/CppTemplateTutorial.cpp @@ -215,15 +215,15 @@ namespace _2_2_4 template struct Y { typedef X ReboundType; - typedef typename X::MemberType MemberType; #if WRONG_CODE_ENABLED + typedef typename X::MemberType MemberType; typedef WTF MemberType3; #endif static void foo() { X instance0; - X::MemberType instan + typename X::MemberType instance1; WTF instance2 大王叫我来巡山 - + & } @@ -233,10 +233,35 @@ namespace _2_2_4 { #if WRONG_CODE_ENABLED Y::foo(); + Y::foo(); #endif } } +namespace _2_3_3 { + struct A; + template + struct X + { + void foo(T v) { + A a; + a.v = v; + } + }; + + struct A + { + int v; + }; + + int foo2() + { + X x; + x.foo(5); + return 0; + } +} + // 1.4 Specialization, Partial Specialization, Full Specialization namespace _1_4 { diff --git a/CppTemplateTutorial.vcxproj b/CppTemplateTutorial.vcxproj index 73cd5bc..2e76f8b 100644 --- a/CppTemplateTutorial.vcxproj +++ b/CppTemplateTutorial.vcxproj @@ -1,5 +1,5 @@  - + Debug @@ -19,13 +19,13 @@ Application true - v110 + v140 Unicode Application false - v110 + v140 true Unicode diff --git a/ReadMe.md b/ReadMe.md index f44ead3..ce3b07d 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1255,7 +1255,7 @@ template struct Y void foo() { X instance0; - X::MemberType instance1; + typename X::MemberType instance1; WTF instance2 大王叫我来巡山 - + & } @@ -1269,12 +1269,256 @@ template struct Y 3. 为什么类型定义3会导致编译错误? 4. 为什么`void foo()`在MSVC下什么错误都没报? -这时我们就需要请出C++11标准了。这是到目前为止,我们第一次阅读标准。我希望能尽量减少直接阅读标准的次数,因此即便是极为复杂的模板匹配决议我都暂时没有引入标准中的描述。 -然而,Template引入的“双阶段名称查找(Two phase name lookup)”堪称是C++中最黑暗的角落 —— 这是LLVM的团队自己在博客上说的 —— 因此在这里阅读标准是必要的。 +这时我们就需要请出C++11标准 —— 中的某些概念了。这是我们到目前为止第一次参阅标准。我希望能尽量减少直接参阅标准的次数,因此即便是极为复杂的模板匹配决议我都暂时没有引入标准中的描述。 +然而,Template引入的“双阶段名称查找(Two phase name lookup)”堪称是C++中最黑暗的角落 —— 这是LLVM的团队自己在博客上说的 —— 因此在这里,我们还是有必要去了解标准中是如何规定的。 ####2.3.2 名称查找:I am who I am -在C++标准中,名称查找(name lookup)集中出现在三处。第一处是3.4节,标题名就叫“Name Lookup”;第二处在10.2节,继承关系中的名称查找;第三处在14.6节,名称解析(name resolution) +在C++标准中对于“名称查找(name lookup)”这个高大上的名词的诠释,主要集中出现在三处。第一处是3.4节,标题名就叫“Name Lookup”;第二处在10.2节,继承关系中的名称查找;第三处在14.6节,名称解析(name resolution)。 +名称查找/名称解析,是编译器的基石。对编译原理稍有了解的人,都知道“符号表”的存在即重要意义。考虑一段最基本的C代码: +``` C +int a = 0; +int b; +b = (a + 1) * 2; +printf("Result: %d", b); +``` +在这段代码中,所有出现的符号可以分为以下几类: + +* `int`:类型标识符,代表整型; +* `a`,`b`,`printf`:变量名或函数名; +* `=`,`+`,`*`:运算符; +* `,`,`;`,`(`,`)`:分隔符; + +那么,编译器怎么知道`int`就是整数类型,`b=(a+1)*2`中的`a`和`b`就是整型变量呢?这就是名称查找/名称解析的作用:它告诉编译器,这个标识符(identifer)是在哪里被声明或定义的,它究竟是什么意思。 + +也正因为这个机制非常基础,所以它才会面临各种可能的情况,编译器也要想尽办法让它在大部分场合都表现的合理。比如我们常见的作用域规则,就是为了对付名称在不同代码块中传播、并且遇到重名要如何处理的问题。下面是一个最简单的、大家在语言入门过程中都会碰到的一个例子: +``` C++ +int a = 0; +void f() { + int a = 0; + a += 2; + printf("Inside : %d\n", a); +} +void g() { + printf("Outside : %d\n", a); +} +int main() { + f(); + g(); +} + +/* ------------ Console Output ----------------- +Inside : 2 +Outside : 0 +--------------- Console Output -------------- */ +``` + +我想大家尽管不能处理所有名称查找中所遇到的问题,但是对一些常见的名称查找规则也有了充分的经验,可以解决一些常见的问题。 +但是模板的引入,使得名称查找这一本来就不简单的基本问题变得更加复杂了。 +考虑下面这个例子: +``` C++ +struct A { int a; }; +struct AB { int a, b; }; +struct C { int c; }; + +template foo(T& v0, C& v1){ + v0.a = 1; + v1.a = 2; + v1.c = 3; +} +``` +简单分析上述代码很容易得到以下结论: + +1. 函数`foo`中的变量`v1`已经确定是`struct C`的实例,所以,`v1.a = 2;`会导致编译错误,`v1.c = 3;`是正确的代码; +2. 对于变量`v0`来说,这个问题就变得很微妙。如果`v0`是`struct A`或者`struct AB`的实例,那么`foo`中的语句`v0.a = 1;`就是正确的。如果是`struct C`,那么这段代码就是错误的。 + +因此在模板定义的地方进行语义分析,并不能**完全**得出代码是正确或者错误的结论,只有到了实例化阶段,确定了模版参数的类型后,才知道这段代码正确与否。令人高兴的是,在这一问题上,我们和C++标准委员会的见地一致,说明我们的C++水平已经和Herb Sutter不分伯仲了。既然我们和Herb Sutter水平差不多,那凭什么人家就吃香喝辣?下面我们来选几条标准看看服不服: + +> ###14.6 名称解析(Name resolution) +**1)** 模板定义中能够出现以下三类名称: + —— 模板名称、或模板实现中所定义的名称; + —— 和模板参数有关的名称; + —— 模板定义所在的定义域内能看到的名称。 + … +**9)** … 如果名字查找和模板参数有关,那么查找会延期到模板参数全都确定的时候。 … +**10)** 如果(模板定义内出现的)名字和模板参数无关,那么在模板定义处,就应该找得到这个名字的声明。… +> ###14.6.2 依赖性名称(Dependent names) +**1)** …(模板定义中的)表达式和类型可能会依赖于模板参数,并且模板参数会影响到名称查找的作用域 … 如果表达式中有操作数依赖于模板参数,那么整个表达式都依赖于模板参数,名称查找延期到**模板实例化时**进行。并且定义时和实例化时的上下文都会参与名称查找。(依赖性)表达式可以分为类型依赖(类型指模板参数的类型)或值依赖。 +> ####14.6.2.2 **类型依赖的表达式** +**2)** 如果成员函数所属的类型是和模板参数有关的,那么这个成员函数中的`this`就认为是类型依赖的。 +> ###14.6.3 非依赖性名称(Non-dependent names) +**1)** 非依赖性名称在**模板定义**时使用通常的名称查找规则进行名称查找。 + +[Working Draft: Standard of Programming Language C++, N3337][1] + +知道差距在哪了吗:人家会说黑话。什么时候咱们也会说黑话了,就是标准委员会成员了,反正懂得也不比他们少。不过黑话确实不太好懂 —— 怪我翻译不好的人,自己看原文,再说好懂了人家还靠什么吃饭 —— 我们来举一个例子: + +```C++ +int a; +struct B { int v; } +template struct X { + B b; // B 是第三类名字,b 是第二类 + T t; // T 是第二类 + X* anthor; // X 这里代指 X,第一类 + typedef int Y; // int 是第三类 + Y y; // Y 是第一类 + C c; // C 什么都不是,编译错误。 + void foo() { + b.v += y; // b 是第一类,非依赖性名称 + b.v *= T::s_mem; // T::s_mem 是第二类 + // s_mem的作用域由T决定 + // 依赖性名称,类型依赖 + } +}; +``` + +所以,按照标准的意思,名称查找会在模板定义和实例化时各做一次,分别处理非依赖性名称和依赖性名称的查找。这就是“两阶段名称查找”这一名词的由来。只不过这个术语我也不知道是谁发明的,它并没有出现的标准上,但是频繁出现在StackOverflow和Blog上。 + +接下来,我们就来解决2.3.1节中留下的几个问题。 + +先看第四个问题。为什么MSVC中,模板函数的定义内不管填什么编译器都不报错?因为MSVC在分析模板定义时没有做任何事情。至于为啥连“大王叫我来巡山”都能过得去,这是C++语法/语义分析的特殊性导致的。 +C++是个非常复杂的语言,以至于它的编译器,不可能通过词法-语法-语义多趟分析清晰分割。因为它的语义将会直接干扰到语法: + +```C++ +void foo(){ + A b; +} +``` +在这段简短的代码中,就包含了两个歧义的可能,一是`A`是模板,于是`A`是一个实例化的类型,`b`是变量,另外一种就是关系表达式,`((A < T) > b)`。甚至词法分析也会受到语义的干扰,C++11中才明确被修正的`vector>`,就因为`>>`被误解为右移或流操作符,而导致某些编译器上的错误。因此,在语义没有确定之前,连语法都没有分析的价值。 +大约是基于如此考量,为了偷懒,MSVC将包括所有的语法/语义分析工作都挪到了第二个Phase,于是乎连带着语法分析都送进了第二个阶段。符合标准么?显然不符合。 +但是这里值得一提的是,MSVC的做法和标准相比,虽然投机取巧,但并非有弊无利。我们来先说一说坏处。考虑以下例子: +```C++ +// ----------- X.h ------------ + +template struct X { + // 实现代码 +}; + +// ---------- X.cpp ----------- + +// ... 一些代码 ... +X xi; +// ... 一些代码 ... +X xf; +// ... 一些代码 ... +``` +此时如果X中有一些与模板参数无关的错误,如果名称查找/语义分析在两个阶段完成,那么这些错误会很早、且唯一的被提示出来;但是如果一切都在实例化时处理,那么可能会导致不同的实例化过程提示同样的错误。而模板在运用过程中,往往会产生很多实例,此时便会大量报告同样的错误。 +当然,MSVC并不会真的这么做。根据推测,最终他们是合并了相同的错误。因为即便对于模板参数相关的编译错误,也只能看到最后一次实例化的错误信息: +``` +template struct X {}; + +template struct Y +{ + typedef X ReboundType; // 类型定义1 + void foo() + { + X instance0; + X::MemberType instance1; + WTF instance2 + } +}; + +void poo(){ + X::foo(); + X::foo(); +} +``` + +MSVC下和模板相关的错误只有一个: +``` +error C2039: 'MemberType': is not a member of 'X' + with + [ + T=float + ] +``` +然后是一些语法错误,比如`MemberType`不是一个合法的标识符之类的。这样甚至你会误以为`int`情况下模板的实力化是正确的。虽然在有了经验之后会发现这个问题挺荒唐的,但是仍然会让新手有困惑。 + +相比之下,更加遵守标准的Clang在错误提示上就要清晰许多: + +``` +error: unknown type name 'WTF' + WTF instance2 + ^ +error: expected ';' at end of declaration + WTF instance2 + ^ + ; +error: no type named 'MemberType' in 'X' + typename X::MemberType instance1; + ~~~~~~~~~~~~~~~^~~~~~~~~~ + note: in instantiation of member function 'Y::foo' requested here + Y::foo(); + ^ +error: no type named 'MemberType' in 'X' + typename X::MemberType instance1; + ~~~~~~~~~~~~~~~^~~~~~~~~~ + note: in instantiation of member function 'Y::foo' requested here + Y::foo(); + ^ +4 errors generated. +``` +可以看到,Clang的提示和标准更加契合。它很好地区分了模板在定义和实例化时分别产生的错误。 +另一个缺点也与之类似。因为没有足够的检查,如果你写的模板没有被实例化,那么很可能缺陷会一直存在于代码之中。特别是模板代码多在头文件。虽然不如接口那么重要,但也是属于被公开的部分,别人很可能会踩到坑上。缺陷一旦传播开修复起来就没那么容易了。 + +但是正如我前面所述,这个违背了标准的特性,并不是一无是处。首先,它可以完美的兼容标准。符合标准的、能够被正确编译的代码,一定能够被MSVC的方案所兼容。其次,它带来了一个非常有趣的特性,看下面这个例子: + +```C++ +struct A; +template struct X { + void foo(T v) { + A a; + a.v = v; + } +}; + +struct A { int v; }; + +void main() { + X x; + x.foo(5); +} +``` +这个例子在Clang中是错误的,因为: +``` +error: variable has incomplete type 'A' + A a; + ^ + note: forward declaration of 'A' + struct A; + ^ +1 error generated. +``` + +符合标准的写法需要将模板类的定义,和模板函数的定义分离开: + +```C++ +struct A; +template struct X { + void foo(T v) { + A a; + a.v = v; + } +}; + +struct A { int v; }; + +template void X::foo(T v) { + A a; + a.v = v; +} + +void main() { + X x; + x.foo(5); +} +``` + +但是其实我们知道,`foo`要到实例化之后,才需要真正的做语义分析。在MSVC上,因为函数实现就是到模板实例化时才处理的,所以这个例子是完全正常工作的。 +在实际应用中,我们经常既希望把模板类成员函数的声明和实现放到一起,因为模板函数看不到实现也很难调用;又希望一般类型可以声明定义分离,把类型定义隐藏到源文件中,以完成声明实现分离。 +这个时候,对于符合标准的编译器,我们只能将模板头文件拆分成``和``两个部分,并按照顺序引用两个文件。但是在MSVC中就可以直接将模板函数的实现,和一般类型的声明放在一起,反而更加简单清晰。 + +扩展阅读: [The Dreaded Two-Phase Name Lookup][2] ###2.4 函数模板的重载、参数匹配、特化与部分特化 ###2. 技巧单元:模板与继承 @@ -1314,3 +1558,7 @@ alexandrescu 关于 min max 的讨论:《再谈Min和Max》 ###7.5 更高更快更强:从Linq到FP ## 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 \ No newline at end of file