Merge pull request #33 from Alinshans/master

📝 Made some changes.
This commit is contained in:
Zihan Chen 2019-09-02 19:10:22 -07:00 committed by GitHub
commit 0ea72a0237
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -5,7 +5,7 @@
### 0.1 C++另类简介:比你用的复杂,但比你想的简单
C++似乎从为世人所知的那天开始便成为天然的话题性编程语言。在它在周围有着形形色色的赞美与贬低之词。当我在微博上透露欲写此文的意愿时,也收到了很多褒贬不一的评论。作为一门语言,能拥有这么多使用并恨着它、使用并畏惧它的用户,也算是语言丛林里的奇观了。
C++似乎从为世人所知的那天开始便成为天然的话题性编程语言。在它在周围有着形形色色的赞美与贬低之词。当我在微博上透露欲写此文的意愿时,也收到了很多褒贬不一的评论。作为一门语言,能拥有这么多使用并恨着它、使用并畏惧它的用户,也算是语言丛林里的奇观了。
C++之所以变成一门层次丰富、结构多变、语法繁冗的语言是有着多层次的原因的。Bjarne在《The Design and Evolution of C++》一书中详细的解释了C++为什么会变成如今C++98/03的模样。这本书也是我和陈梓瀚一直对各位已经入门的新手强烈推荐的一本书。通过它你多少可以明白C++的诸多语法要素之所以变成如今的模样,实属迫不得已。
@ -19,7 +19,7 @@ C++之所以变成一门层次丰富、结构多变、语法繁冗的语言,
2002年出版的另一本书《C++ Templates》可以说是在Template方面的集大成之作。它详细阐述了模板的语法、提供了和模板有关的语言细节信息举了很多有代表性例子。但是对于模板新手来说这本书细节如此丰富让他们随随便便就打了退堂鼓缴械投降。
本文的写作初衷,就是通过“编程语言”的视角,介绍一个简单、清晰的“模板语言”。我会尽可能将模板的诸多要素连串起来,用一些简单的例子帮助读者学习这门“语言”,让读者在编写、阅读模板代码的时候,能像 `if(exp) { dosomething(); }`一样的信手拈来,让“模板元编程”技术成为读者牢固掌握、可举一反三的有用技能。
本文的写作初衷,就是通过“编程语言”的视角,介绍一个简单、清晰的“模板语言”。我会尽可能将模板的诸多要素连串起来,用一些简单的例子帮助读者学习这门“语言”,让读者在编写、阅读模板代码的时候,能像 `if(exp) { dosomething(); }`一样的信手拈来,让“模板元编程”技术成为读者牢固掌握、可举一反三的有用技能。
### 0.2 适宜读者群
@ -115,7 +115,7 @@ template <typename T> class ClassA
void foo(int a);
```
`T`则可以类比为函数形参`a`,这里的“模板形参”`T`,也同函数形参一样取成任何你想要的名字;`typename`则类似于例子中函数参数类型`int`,它表示模板参数中的`T`将匹配一个类型。除了 `typename` 之外,我们后面还要讲到,整型也可以作为模板的参数。
`T`则可以类比为函数形参`a`,这里的“模板形参”`T`,也同函数形参一样取成任何你想要的名字;`typename`则类似于例子中函数参数类型`int`,它表示模板参数中的`T`将匹配一个类型。除了 `typename` 之外,我们后面还要讲到,整型也可以作为模板的参数。
在定义完模板参数之后,便可以定义你所需要的类。不过在定义类的时候,除了一般类可以使用的类型外,你还可以使用在模板参数中使用的类型 `T`。可以说,这个 `T`是模板的精髓因为你可以通过指定模板实参将T替换成你所需要的类型。
@ -162,7 +162,7 @@ floatArray.push_back(3.0f);
```
变量定义的过程可以分成两步来看:第一步,`vector<int>`将`int`绑定到模板类`vector`上,获得了一个“普通的类`vector<int>`”第二步通过“vector<int>”定义了一个变量。
与“普通的类”不同,模板类是不能直接用来定义变量的。例如
与“普通的类”不同,模板类是不能直接用来定义变量的。例如
```C++
vector unknownVector; // 错误示例
@ -299,7 +299,7 @@ template <typename T> void foo()
如何才能克服这一问题,最终视模板如平坦代码呢?
答案只有一个:无他,唯手熟尔。
答案只有一个:**无他,唯手熟尔**
在学习模板的时候,要反复做以下的思考和练习:
@ -365,7 +365,7 @@ int b = 3;
int result = Add(a, b);
```
编译器会心领神会`Add` 变成 `Add<int>`。但是编译器不能面对模棱两可的答案。比如你这么写的话呢?
编译器会心领神会`Add` 变成 `Add<int>`。但是编译器不能面对模棱两可的答案。比如你这么写的话呢?
``` C++
int a = 5;
@ -390,7 +390,7 @@ template <typename T> class A {};
template <typename T> T foo( A<T> v );
A<int> v;
foo(v); // 它能准确猜到 T 是 int.
foo(v); // 它能准确猜到 T 是 int.
```
编译器居然绕过了A这个外套猜到了 `T` 匹配的是 `int`。编译器是怎么完成这一“魔法”的我们暂且不表2.2节时再和盘托出。
@ -452,7 +452,7 @@ float i = c_style_cast<int, float>(v);
很顺利的通过了。难道C++不能支持让参数推导一部分模板参数吗?
当然是可以的。只不过在部分推导、部分指定的情况下,编译器对模板参数的顺序是有限制的:先写需要指定的模板参数,再把能推导出来的模板参数放在后面。
当然是可以的。只不过在部分推导、部分指定的情况下,编译器对模板参数的顺序是有限制的:**先写需要指定的模板参数,再把能推导出来的模板参数放在后面**
在这个例子中,能推导出来的是 `SrcT`,需要指定的是 `DstT`。把函数模板写成下面这样就可以了:
@ -468,7 +468,7 @@ float i = c_style_cast<float>(v); // 形象地说DstT会先把你指定的
### 1.3 整型也可是Template参数
模板参数除了类型外包括基本类型、结构、类类型等也可以是一个整型数Integral Number。这里的整型数比较宽泛包括布尔不同位数、有无符号的整型,甚至包括指针。我们将整型的模板参数和类型作为模板参数来做一个对比:
模板参数除了类型外包括基本类型、结构、类类型等也可以是一个整型数Integral Number。这里的整型数比较宽泛包括布尔型,不同位数、有无符号的整型,甚至包括指针。我们将整型的模板参数和类型作为模板参数来做一个对比:
``` C++
template <typename T> class TemplateWithType;
@ -499,7 +499,7 @@ class IntArrayWithSize16
IntArrayWithSize16 arr;
```
其中有一点要注意的是,因为模板的匹配是在编译的时候完成的,所以实例化模板的时候所使用的参数,也必须要在编译期就能确定。例如以下的例子编译器就会报错:
其中有一点要注意,因为模板的匹配是在编译的时候完成的,所以实例化模板的时候所使用的参数,也必须要在编译期就能确定。例如以下的例子编译器就会报错:
``` C++
template <int i> class A {};
@ -559,7 +559,7 @@ template <float a> class E {}; // ERROR: 别闹!早说过只能是整数类型
这个问题很功利但是一针见血。因为技术的根本目的在于解决需求。那C++的模板能做什么?
一个高(树)大(新)上(的回答是C++里面的模板犹如C中的宏、C#和Java中的自省restropection和反射reflection一样,是一个改变语言内涵,拓展语言外延的存在。
一个高(树)大(新)上(的回答是C++里面的模板犹如C中的宏、C#和Java中的自省restropection和反射reflection是一个改变语言内涵拓展语言外延的存在。
程序最根本的目的是什么?复现真实世界或人所构想的规律,减少重复工作的成本,或通过提升规模完成人所不能及之事。但是世间之事万千,有限的程序如何重现复杂的世界呢?
@ -648,13 +648,13 @@ typedef Stack<int> StackInt;
typedef Stack<float> StackFloat;
```
通过模板,我们可以将形形色色的堆栈代码分为两个部分,一个部分是不变的接口,以及近乎相同的实现;另外一部分是元素的类型,它们是需要变化的。因此同函数类似,需要变化的部分,由模板参数来反;不变的部分,则是模板内的代码。可以看到,使用模板的代码,要比不使用模板的代码简洁许多。
通过模板,我们可以将形形色色的堆栈代码分为两个部分,一个部分是不变的接口,以及近乎相同的实现;另外一部分是元素的类型,它们是需要变化的。因此同函数类似,需要变化的部分,由模板参数来反;不变的部分,则是模板内的代码。可以看到,使用模板的代码,要比不使用模板的代码简洁许多。
如果元编程中所有变化的量(或者说元编程的参数),都是类型,那么这样的编程,我们有个特定的称呼,叫“泛型”。
如果元编程中所有变化的量(或者说元编程的参数),都是类型,那么这样的编程,我们有个特定的称呼,叫“泛型”。
但是你会问,模板的发明,仅仅是为了做和宏几乎一样的替换工作吗?可以说是,也可以说不是。一方面,很多时候模板就是为了替换类型,这个时候作用上其实和宏没什么区别。只是宏是基于文本的替换,被替换的文本本身没有任何语义。只有替换完成,编译器才能进行接下来的处理。而模板会在分析模板时以及实例化模板时时候都会进行检查,而且源代码中也能与调试符号一一对应,所以无论是编译时还是运行时,排错都相对简单。
但是模板和宏有很大的不同否则此文也就不能成立了。模板最大的不同在于它是“可以运算”的。我们来举一个例子不过可能有点牵强。考虑我们要写一个向量逐分量乘法。只不过这个向量它非常的大。所以为了保证速度我们需要使用SIMD指令进行加速。假设我们有以下指令可以使用
但是模板和宏有很大的不同否则此文也就不能成立了。模板最大的不同在于它是“可以运算”的。我们来举一个例子不过可能有点牵强。考虑我们要写一个向量逐分量乘法。只不过这个向量它非常的大。所以为了保证速度我们需要使用SIMD指令进行加速。假设我们有以下指令可以使用
```
Int8,16: N/A
@ -791,7 +791,7 @@ int main()
}
```
这点限制也粉碎了妄图用模板来包办工厂Factory甚至是反射的梦想。尽管在《Modern C++ Design》中别问我为什么老举这本书因为《C++ Templates》和《Generic Programming》我只是囫囵吞枣读过基本不记得了)大量运用模板来简化工厂方法同时C++1114中的一些机制如Variadic Template更是让这一问题的解决更加彻底。但无论如何直到C++11/14光靠模板你就是写不出依靠类名或者ID变量产生类型实例的代码。
这点限制也粉碎了妄图用模板来包办工厂Factory甚至是反射的梦想。尽管在《Modern C++ Design》中别问我为什么老举这本书因为《C++ Templates》和《Generic Programming》我只是囫囵吞枣读过基本不记得了)大量运用模板来简化工厂方法同时C++11/14中的一些机制如Variadic Template更是让这一问题的解决更加彻底。但无论如何直到C++11/14光靠模板你就是写不出依靠类名或者ID变量产生类型实例的代码。
所以说,从能力上来看,模板能做的事情都是编译期完成的。编译期完成的意思就是,当你编译一个程序的时候,所有的量就都已经确定了。比如下面的这个例子:
@ -877,7 +877,7 @@ void foo()
z = AddFloatOrMulInt<int>::Do(x, y); // z = x * y;
}
```
也许你不明白为什么要改写成现在这个样子。看不懂不怪你,怪我讲不好。但是你别急,先看看这样改写以后能不能跟我们的目标接近一点。如果我们把 `AddFloatOrMulInt<float>::Do` 看作一个普通的函数,那么我们可以写两个实现出来:
也许你不明白为什么要改写成现在这个样子。看不懂不怪你,怪我讲不好。但是你别急,先看看这样改写以后能不能跟我们的目标接近一点。如果我们把 `AddFloatOrMulInt<float>::Do` 看作一个普通的函数,那么我们可以写两个实现出来:
``` C++
float AddFloatOrMulInt<float>::Do(float a, float b)
@ -1041,7 +1041,7 @@ template <> class TypeToID<float>
};
```
嗯, 这个你已经了然于心了。那么`void*`呢?你想了想,这已经是一个复合类型了。不错你还是战战兢兢写了下来:
嗯, 这个你已经了然于心了。那么`void*`呢?你想了想,这已经是一个复合类型了。不错你还是战战兢兢写了下来:
``` C++
template <> class TypeToID<void*>
@ -1066,7 +1066,7 @@ template <> class TypeToID<int (int[3])>; // 这是以数组为参数的函数
template <> class TypeToID<int (ClassB::*[3])(void*, float[2])>; // 我也不知道这是什么了,自己看着办吧。
```
甚至连 `const``volatile` 都能装进去
甚至连 `const``volatile` 都能装进去
``` C++
template <> class TypeToID<int const * volatile * const volatile>;
@ -1083,7 +1083,7 @@ void PrintID()
嗯,它输出的是-1。我们顺藤摸瓜会看到 `TypeToID`的类模板“原型”的ID是值就是-1。通过这个例子可以知道当模板实例化时提供的模板参数不能匹配到任何的特化形式的时候它就会去匹配类模板的“原型”形式。
不过这里有一个问题要清一下。和继承不同,类模板的“原型”和它的特化类在实现上是没有关系的,并不是在类模板中写了 `ID` 这个Member那所有的特化就必须要加入 `ID` 这个Member或者特化就自动有了这个成员。完全没这回事。我们把类模板改成以下形式或许能看的更清楚一点
不过这里有一个问题要清一下。和继承不同,类模板的“原型”和它的特化类在实现上是没有关系的,并不是在类模板中写了 `ID` 这个Member那所有的特化就必须要加入 `ID` 这个Member或者特化就自动有了这个成员。完全没这回事。我们把类模板改成以下形式或许能看的更清楚一点
``` C++
template <typename T> class TypeToID
@ -1138,13 +1138,13 @@ void copy(void* dst, void const* src, size_t elemSize, size_t elemCount, void (*
template <typename T>
```
接下来,我们要写函数原型:
接下来,我们要写函数原型
``` C++
void copy(?? dest, ?? src, size_t elemCount);
```
这里的 `` 要怎么写呢既然我们有了模板类型参数T那我们不如就按照经验`T*` 看看。
这里的 `??` 要怎么写呢既然我们有了模板类型参数T那我们不如就按照经验`T*` 看看。
``` C++
template <typename T>
@ -1179,7 +1179,7 @@ public:
};
```
最后写个例子来测试一下,看看我们的 `T*` 能不能搞定 `float*`
最后写个例子来测试一下,看看我们的 `T*` 能不能搞定 `float*`
``` C++
void PrintID()
@ -1221,14 +1221,15 @@ OK猜出来了吗T是`float`。为什么呢?因为你用 `float *` 匹
template <typename T>
class RemovePointer
{
// 啥都不干,你要放一个不是指针的类型进来,我就让你死的难看。
public:
typedef T Resylt; // 如果放进来的不是一个指针,那么它就是我们要的结果。
};
template <typename T>
class RemovePointer<T*> // 祖传牛皮藓,专治各类指针
{
public:
typedef T Result;
typedef T Result; // 正如我们刚刚讲的,去掉一层指针,把 T* 这里的 T 取出来。
};
void Foo()
@ -1238,7 +1239,23 @@ void Foo()
}
```
OK如果这个时候我需要给 `int*` 提供一个更加特殊的特化,那么我还得都多提供一个:
当然啦,这里我们实现的不算是真正的 `RemovePointer`,因为我们只去掉了一层指针。而如果传进来的是类似 `RemovePointer<int**>` 这样的东西呢?是的没错,去掉一层之后还是一个指针。`RemovePointer<int**>::Result` 应该是一个 `int*`,要怎么才能实现我们想要的呢?聪明的你一定能想到:只要像剥洋葱一样,一层一层一层地剥开,不就好了吗!相应地我们应该怎么实现呢?可以把 `RemovePointer` 的特化版本改成这样(当然如果有一些不明白的地方你可以暂时跳过,接着往下看,很快就会明白的):
``` C++
template <typename T>
class RemovePointer<T*>
{
public:
// 如果是传进来的是一个指针,我们就剥夺一层,直到指针形式不存在为止。
// 例如 RemovePointer<int**>Result 是 RemovePointer<int*>::Result
// 而 RemovePointer<int*>::Result 又是 int最终就变成了我们想要的 int其它也是类似。
typedef typename RemovePointer<T>::Result Result;
};
```
是的没错,这便是我们想要的 `RemovePointer` 的样子。类似的你还可以试着实现 `RemoveConst`, `AddPointer` 之类的东西。
OK回到我们之前的话题如果这个时候我需要给 `int*` 提供一个更加特殊的特化,那么我还得多提供一个:
``` C++
// ...
@ -1254,7 +1271,7 @@ public:
};
template <> // 嗯int* 已经是个具体的不能再具体的类型了,所以模板不需要额外的类型参数了
class TypeToID<int*> // 嗯对int*的特化。在这里呢要把int*整体看作一个类型
class TypeToID<int*> // 嗯对int*的特化。在这里呢要把int*整体看作一个类型
{
public:
static int const ID = 0x12345678; // 给一个缺心眼的ID
@ -1268,9 +1285,9 @@ void PrintID()
这个时候它会输出0x12345678的十进制大概
可能会有较真的人说,`int*` 去匹配 `T` 或者 `T*`也是合法的。就和你说22岁以上能结婚那24岁当然也能结婚一样。
那为什么 `int*` 就会找 `int*``float *`因为没有合适的特化就去找 `T*`,更一般的就去找 `T` 呢?废话,有专门为你准备的东西的不用,人干事?这就是直觉。
那为什么 `int*` 就会找 `int*``float *`因为没有合适的特化就去找 `T*`,更一般的就去找 `T` 呢?废话,有专门为你准备的东西你不用,非要自己找事?这就是直觉。
但是呢,直觉对付更加复杂的问题还是没用的(也不是没用,主要是你没这个直觉了)。我们要把这个直觉,转换成合理的规则——即模板的匹配规则。
当然,这个匹配规则是对复杂问题用的,所以我们会到实在一眼看不出来的时候才会动用它。一开始我们只要把握:模板是从最特殊到最一般形式进行匹配就可以了。
当然,这个匹配规则是对复杂问题用的,所以我们会到实在一眼看不出来的时候才会动用它。一开始我们只要把握:**模板是从最特殊到最一般形式进行匹配的** 就可以了。
### 2.3 即用即推导
@ -1313,7 +1330,7 @@ template <typename T> struct Y
#### 2.3.2 名称查找I am who I am
在C++标准中对于“名称查找name lookup”这个高大上的名词的诠释主要集中出现在三处。第一处是3.4节标题名就叫“Name Lookup”第二处在10.2节继承关系中的名称查找第三处在14.6节名称解析name resolution
名称查找/名称解析,是编译器的基石。对编译原理稍有了解的人,都知道“符号表”的存在重要意义。考虑一段最基本的C代码
名称查找/名称解析,是编译器的基石。对编译原理稍有了解的人,都知道“符号表”的存在重要意义。考虑一段最基本的C代码
``` C
int a = 0;
int b;
@ -1426,7 +1443,7 @@ template <typename T> struct X {
接下来我们就来解决2.3.1节中留下的几个问题。
先看第四个问题。为什么MSVC中模板函数的定义内不管填什么编译器都不报错因为MSVC在分析模板中成员函数定义时没有做任何事情。至于为啥连“大王叫我来巡山”都能过得去这是C++语法/语义分析的特殊性导致的。
C++是个非常复杂的语言,以至于它的编译器,不可能通过词法-语法-语义多趟分析清晰分割因为它的语义将会直接干扰到语法:
C++是个非常复杂的语言,以至于它的编译器,不可能通过词法-语法-语义多趟分析清晰分割因为它的语义将会直接干扰到语法:
```C++
void foo(){
@ -1734,7 +1751,7 @@ DoWork<int> i; // (4)
DoWork<float*> pf; // (5)
```
首先,编译器分析(0), (1), (2)三句,得知(0)是模板的原型,(1)(2)(3)是模板(0)的特化或偏特化。我们假设有两个字典,第一个字典存储了模板原型,我们称之为`TemplateDict`。第二个字典`TemplateSpecDict`,存储了模板原型所对应的特化/偏特化形式。所以编译器在这几句时,可以视作
首先,编译器分析(0), (1), (2)三句,得知(0)是模板的原型,(1)(2)(3)是模板(0)的特化或偏特化。我们假设有两个字典,第一个字典存储了模板原型,我们称之为`TemplateDict`。第二个字典`TemplateSpecDict`,存储了模板原型所对应的特化/偏特化形式。所以编译器在处理这几句时,可以视作
```C++
// 以下为伪代码
@ -1812,7 +1829,7 @@ X<double*, double> v8;
>`type [i]`, `template-name <i>`, `TT<T>`, `TT<i>`, `TT<>`
对于某些实例化偏特化的选择并不是唯一的。比如v4的参数是`<float*, float*>`能够匹配的就有三条规则16和7。很显然6还是比7好一些因为能多匹配一个指针。但是1和6就很难说清楚谁更好了。一个说明了两者类型相同另外一个则说明了两者都是指针。所以在这里编译器也没办法决定使用那个只好出了编译器错误。
对于某些实例化偏特化的选择并不是唯一的。比如v4的参数是`<float*, float*>`能够匹配的就有三条规则16和7。很显然6还是比7好一些因为能多匹配一个指针。但是1和6就很难说清楚谁更好了。一个说明了两者类型相同另外一个则说明了两者都是指针。所以在这里编译器也没办法决定使用那个只好出了编译器错误。
其他的示例可以先自己推测一下, 再去编译器上尝试一番:[`goo.gl/9UVzje`](http://goo.gl/9UVzje)。
@ -2217,7 +2234,7 @@ void callFoo() {
}
```
那么 `foo( A() )` 虽然匹配 `foo(B const&)` 会失败,但是它起码能匹配 `foo(A const&)`,所以它是正确的;`foo( B() )` 能同时匹配两个函数原型,但是 `foo(B const&)` 要更好一些,因此它选择了这个原型。而 `foo( C() );` 因为两个函数都匹配失败Failure所以它找不到相应的原型这时才会出一个编译器错误Error
那么 `foo( A() )` 虽然匹配 `foo(B const&)` 会失败,但是它起码能匹配 `foo(A const&)`,所以它是正确的;`foo( B() )` 能同时匹配两个函数原型,但是 `foo(B const&)` 要更好一些,因此它选择了这个原型。而 `foo( C() );` 因为两个函数都匹配失败Failure所以它找不到相应的原型这时才会出一个编译器错误Error
所以到这里我们就明白了在很多情况下Failure is not an error。编译器在遇到Failure的时候往往还需要尝试其他的可能性。