diff --git a/CppTemplateTutorial.cpp b/CppTemplateTutorial.cpp index d300dc5..fbd07ec 100644 --- a/CppTemplateTutorial.cpp +++ b/CppTemplateTutorial.cpp @@ -1,7 +1,8 @@ #include "stdafx.h" #include +#include -#define WRONG_CODE_ENABLED 1 +#define WRONG_CODE_ENABLED 0 // 0. Basic Form namespace _0 @@ -92,6 +93,31 @@ namespace _1_2_2 // 1.3 Instanciating 2 namespace _1_3 { + template class A + { + public: + void foo() + { + } + }; + template class B {}; + template class C {}; + template ::*a)()> class D {}; + template class E {}; + void foo() + { + A<5> a; + B<7, A<5>, nullptr> b; + C<&foo> c; + D<&A<3>::foo> d; +#if WRONG_CODE_ENABLED + int x = 3; + A b; +#endif + } + + + template class ClassB { diff --git a/ReadMe.md b/ReadMe.md index 8ed63c2..a25b9f4 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -69,7 +69,7 @@ template class ClassA void foo(int a); ``` -`T`则可以类比为函数形参`a`,这里的“模板形参”`T`,也同函数形参一样取成任何你想要的名字;`typename`则类似于例子中函数参数类型`int`,它表示模板参数中的`T`将匹配一个类型。 +`T`则可以类比为函数形参`a`,这里的“模板形参”`T`,也同函数形参一样取成任何你想要的名字;`typename`则类似于例子中函数参数类型`int`,它表示模板参数中的`T`将匹配一个类型。除了 `typename` 之外,我们再后面还要讲到,整型也可以作为模板的参数。 在定义完模板参数之后,便可以定义你所需要的类。不过在定义类的时候,除了一般类可以使用的类型外,你还可以使用在模板参数中使用的类型 `T`。可以说,这个 `T`是模板的精髓,因为你可以通过指定模板实参,将T替换成你所需要的类型。 @@ -197,7 +197,7 @@ void vector::clear() 因此,在成员函数实现的时候,必须要提供模板参数。此外,为什么类型名不是`vector`而是`vector`呢? 如果你了解过模板的偏特化与特化的语法,应该能看出,这里的vector在语法上类似于特化/偏特化。实际上,这里的函数定义也确实是成员函数的偏特化。特化和偏特化的概念,本文会在第二部分详细介绍。 -最终,正确的成员函数实现如下所示: +综上,正确的成员函数实现如下所示: ``` C++ template // 模板参数 @@ -363,8 +363,7 @@ float a = GetValue(0); // 出错了! int b = GetValue(1); // 也出错了! ``` -为什么会出错呢?你仔细想了想,原来编译器是没办法去根据返回值推断类型的。函数调用的时候,返回值被谁接受还不知道呢。 -如下修改后,就一切正常了: +为什么会出错呢?你仔细想了想,原来编译器是没办法去根据返回值推断类型的。函数调用的时候,返回值被谁接受还不知道呢。如下修改后,就一切正常了: ``` C++ float a = GetValue(0); @@ -373,14 +372,14 @@ int b = GetValue(1); 嗯,是不是so easy啊?嗯,你又信心满满的做了一个练习: -你要写一个模板函数叫 `c_style_cast`,顾名思义,执行的是C风格的转换。然后出于方便起见,你希望它能和 `static_cast` 这样的内置转换有同样的写法。 -于是你写了一个use case。 +你要写一个模板函数叫 `c_style_cast`,顾名思义,执行的是C风格的转换。然后出于方便起见,你希望它能和 `static_cast` 这样的内置转换有同样的写法。于是你写了一个use case。 ``` C++ DstT dest = c_style_cast(src); ``` 根据调用形式你知道了,有 `DstT` 和 `SrcT` 两个模板参数。参数只有一个, `src`,所以函数的形参当然是这么写了: `(SrcT src)`。实现也很简单, `(DstT)v`。 + 我们把手上得到的信息来拼一拼,就可以编写自己的函数模板了: ``` C++ @@ -399,8 +398,7 @@ float i = c_style_cast(v); error C2783: 'DstT _1_2_2::c_style_cast(SrcT)' : could not deduce template argument for 'DstT' ``` -然后你仔细的比较了一下,然后发现 … 模板参数有两个,而参数里面能得到的只有 `SrcT` 一个。结合出错信息看来关键在那个 `DstT` 上。 -这个时候,你死马当活马医,把模板参数写完整了: +然后你仔细的比较了一下,然后发现 … 模板参数有两个,而参数里面能得到的只有 `SrcT` 一个。结合出错信息看来关键在那个 `DstT` 上。这个时候,你死马当活马医,把模板参数写完整了: ``` C++ float i = c_style_cast(v); @@ -409,7 +407,8 @@ float i = c_style_cast(v); 嗯,很顺利的通过了。难道C++不能支持让参数推导一部分模板参数吗? 当然是可以的。只不过在部分推导、部分指定的情况下,编译器对模版参数的顺序是有限制的:先写需要指定的模板参数,再把能推导出来的模板参数放在后面。 -在这个例子中,能推导出来的是 `SrcT`,需要指定的是 `DstT`。于是你把函数模板写成: + +在这个例子中,能推导出来的是 `SrcT`,需要指定的是 `DstT`。把函数模板写成下面这样就可以了: ``` C++ template DstT c_style_cast(SrcT v) // 模版参数 DstT 需要人肉指定,放前面。 @@ -421,9 +420,91 @@ int v = 0; float i = c_style_cast(v); // 形象地说,DstT会先把你指定的参数吃掉,剩下的就交给编译器从函数参数列表中推导啦。 ``` - ###1.3 整型也可是Template参数 +模板参数除了类型外(包括基本类型、结构、类类型等),也可以是一个整型数(Integral Number)。这里的整型数比较宽泛,包括布尔、不同位数、有无符号的整型,甚至包括指针。我们将整型的模板参数和类型作为模板参数来做一个对比: + +``` C++ +template class TemplateWithType; +template class TemplateWithValue; +``` + +我想这个时候你也更能理解 `typename` 的意思了:它相当于是模板参数的“类型”,告诉你 `T` 是一个 `typename`。 + +按照C++ Template最初的想法,模板不就是为了提供一个类型安全、易于调试的宏吗?有类型就够了,为什么要引入整型参数呢?考虑宏,它除了代码替换,还有一个作用是作为常数出现。所以整型模板参数最基本的用途,也是定义一个常数。例如这段代码的作用: + +``` C++ +template struct Array +{ + T data[Size]; +}; + +Array arr; +``` + +便相当于下面这段代码: + +``` C++ +class IntArrayWithSize16 +{ + int data[16]; // int 替换了 T, 16 替换了 Size +}; + +IntArrayWithSize16 arr; +``` + +其中有一点要注意的是,因为模板的匹配是在编译的时候完成的,所以实例化模板的时候所使用的参数,也必须要在编译期就能确定。例如以下的例子编译器就会报错: + +``` C++ +template class A {}; + +void foo() +{ + int x = 3; + A<5> a; // 正确! + A b; // error C2971: '_1_3::A' : template parameter 'i' : 'x' : a local variable cannot be used as a non-type argument +} +``` +因为x不是一个编译期常量,所以 `A` 就会告诉你,x是一个局部变量,不能作为一个模板参数出现。 + +嗯,这里我们再写几个相对复杂的例子: + +``` C++ +template class A +{ +public: + void foo(int) + { + } +}; +template class B {}; +template class C {}; +template ::*a)(int)> class D {}; + +template int Add(int a) // 当然也能用于函数模板 +{ + return a + i; +} + +void foo() +{ + A<5> a; + B< + 7, A<5>, nullptr, false + > b; // 模板参数可以是一个无符号八位整数,可以是模板生成的类;可以是一个指针。 + C c; // 模板参数可以是一个bool类型的常量,甚至可以是一个函数指针。 + D<&A<3>::foo> d; // 丧心病狂啊!它还能是一个成员函数指针! + int x = Add<3>(5); // x == 8。因为整型模板参数无法从函数参数获得,所以只能是手工指定啦。 +} + +template class E {}; // ERROR: 别闹!早说过只能是整数类型的啦! +``` + +###1.4 模板形式与功能是统一的 +第一章走马观花的带着大家复习了一下C++ Template的基本语法形式,也解释了包括 `typename` 在内,类/函数模板写法中各个语法元素的含义。形式是功能的外在体现,介绍它们也是为了让大家能理解到,模板之所以写成这种形式是有必要的,而不是语言的垃圾成分。 + +从下一章开始,我们便进入了更加复杂和丰富的世界:讨论模板的匹配规则。其中有令人望而生畏的特化与偏特化。但是,请相信我们在序言中所提到的:将模板作为一门语言来看待,它会变得有趣而简单。 + ## 2. 模板世界的If-Then-Else:特化与偏特化 ###2.1 类模板的匹配规则:特化与部分特化 ###2.2 函数模板的重载、参数匹配、特化与部分特化