开源软件名称:wuye9036/CppTemplateTutorial开源软件地址:https://github.com/wuye9036/CppTemplateTutorial开源编程语言:C++ 97.0%开源软件介绍:C++ Template 进阶指南章节目录由VSCode插件Markdown All in One生成。
0. 前言0.1 C++另类简介:比你用的复杂,但比你想的简单C++似乎从它为世人所知的那天开始便成为天然的话题性编程语言。在它在周围有着形形色色的赞美与贬低之词。当我在微博上透露欲写此文的意愿时,也收到了很多褒贬不一的评论。作为一门语言,能拥有这么多使用并恨着它、使用并畏惧它的用户,也算是语言丛林里的奇观了。 C++之所以变成一门层次丰富、结构多变、语法繁冗的语言,是有着多层次的原因的。Bjarne在《The Design and Evolution of C++》一书中,详细的解释了C++为什么会变成如今(C++98/03)的模样。这本书也是我和陈梓瀚一直对各位已经入门的新手强烈推荐的一本书。通过它你多少可以明白,C++的诸多语法要素之所以变成如今的模样,实属迫不得已。 模板作为C++中最有特色的语言特性,它堪称玄学的语法和语义,理所应当的成为初学者的梦魇。甚至很多工作多年的人也对C++的模板部分保有充分的敬畏。在多数的编码标准中,Template俨然和多重继承一样,成为了一般程序员(非程序库撰写者)的禁区。甚至运用模板较多的Boost,也成为了“众矢之的”。 但是实际上C++模板远没有想象的那么复杂。我们只需要换一个视角:在C++03的时候,模板本身就可以独立成为一门“语言”。它有“值”,有“函数”,有“表达式”和“语句”。除了语法比较蹩脚外,它既没有指针也没有数组,更没有C++里面复杂的继承和多态。可以说,它要比C语言要简单的多。如果我们把模板当做是一门语言来学习,那只需要花费学习OO零头的时间即可掌握。按照这样的思路,可以说在各种模板书籍中出现的多数技巧,都可以被轻松理解。 简单回顾一下模板的历史。87年的时候,泛型(Generic Programming)便被纳入了C++的考虑范畴,并直接导致了后来模板语法的产生。可以说模板语法一开始就是为了在C++中提供泛型机制。92年的时候,Alexander Stepanov开始研究利用模板语法制作程序库,后来这一程序库发展成STL,并在93年被接纳入标准中。 此时不少人以为STL已经是C++模板的集大成之作,C++模板技止于此。但是在95年的《C++ Report》上,John Barton和Lee Nackman提出了一个矩阵乘法的模板示例。可以说元编程在那个时候开始被很多人所关注。自此篇文章发表之后,很多大牛都开始对模板产生了浓厚的兴趣。其中对元编程技法贡献最大的当属Alexandrescu的《Modern C++ Design》及模板程序库Loki。这一2001年发表的图书间接地导致了模板元编程库的出现。书中所使用的Typelist等泛型组件,和Policy等设计方法令人耳目一新。但是因为全书用的是近乎Geek的手法来构造一切设施,因此使得此书阅读起来略有难度。 2002年出版的另一本书《C++ Templates》,可以说是在Template方面的集大成之作。它详细阐述了模板的语法、提供了和模板有关的语言细节信息,举了很多有代表性例子。但是对于模板新手来说,这本书细节如此丰富,让他们随随便便就打了退堂鼓缴械投降。 本文的写作初衷,就是通过“编程语言”的视角,介绍一个简单、清晰的“模板语言”。我会尽可能地将模板的诸多要素连串起来,用一些简单的例子帮助读者学习这门“语言”,让读者在编写、阅读模板代码的时候,能像 0.2 适宜读者群因为本文并不是用于C++入门,例子中也多少会牵涉一些其它知识,因此如果读者能够具备以下条件,会读起来更加轻松:
此外,尽管第一章会介绍一些Template的基本语法,但是还是会略显单薄。因此也希望读者能对C++ Template最基本语法形式有所了解和掌握;如果会编写基本的模板函数和模板类那就更好了。 诚如上节所述,本文并不是《C++ Templates》的简单重复,与《Modern C++ Design》交叠更少。从知识结构上,我建议大家可以先读本文,再阅读《C++ Templates》获取更丰富的语法与实现细节,以更进一步;《Modern C++ Design》除了元编程之外,还有很多的泛型编程示例,原则上泛型编程的部分与我所述的内容交叉不大,读者在读完1-3章了解模板的基本规则之后便可阅读《MCD》的相应章节;元编程部分(如Typelist)建议在阅读完本文之后再行阅读,或许会更易理解。 0.3 版权本文是随写随即同步到Github上,因此在行文中难免会遗漏引用。本文绝大部分内容应是直接承出我笔,但是也不定会有他山之石。所有指涉内容我会尽量以引号框记,或在上下文和边角注记中标示,如有遗漏烦请不吝指出。 全文所有为我所撰写的部分,作者均保留所有版权。如果有需要转帖或引用,还请注明出处并告知于我。 0.4 推荐编译环境C++编译器众多,且对模板的支持可能存在细微差别。如果没有特别强调,本书行文过程中,使用了下列编译器来测试文中提供的代码和示例:
此外,部分复杂实例我们还在文中提供了在线的编译器预览以方便大家阅读和测试。在线编译器参见: 一些示例中用到的特性所对应的C++标准:
0.5 体例0.5.1 示例代码void SampleCode() {
// 这是一段示例代码
} 0.5.2 引用引用自C++标准:
引用自其他图书:
0.6 意见、建议、喷、补遗、写作计划
1. Template的基本语法1.1 Template Class基本语法1.1.1 Template Class的与成员变量定义我们来回顾一下最基本的Template Class声明和定义形式: Template Class声明: template <typename T> class ClassA; Template Class定义: template <typename T> class ClassA
{
T member;
};
void foo(int a);
在定义完模板参数之后,便可以定义你所需要的类。不过在定义类的时候,除了一般类可以使用的类型外,你还可以使用在模板参数中使用的类型 例如我们用 // 注意:这并不是有效的C++语法,只是为了说明模板的作用
typedef class {
int member;
} ClassA<int>; 可以看出,通过模板参数替换类型,可以获得很多形式相同的新类型,有效减少了代码量。这种用法,我们称之为“泛型”(Generic Programming),它最常见的应用,即是STL中的容器模板类。 1.1.2 模板的使用对于C++来说,类型最重要的作用之一就是用它去产生一个变量。例如我们定义了一个动态数组(列表)的模板类 template <typename T>
class vector
{
public:
void push_back(T const&);
void clear();
private:
T* elements;
}; 此时我们的程序需要一个整型和一个浮点型的列表,那么便可以通过以下代码获得两个变量: vector<int> intArray;
vector<float> floatArray; 此时我们就可以执行以下的操作,获得我们想要的结果: intArray.push_back(5);
floatArray.push_back(3.0f); 变量定义的过程可以分成两步来看:第一步, vector unknownVector; // 错误示例 这样就是错误的。我们把通过类型绑定将模板类变成“普通的类”的过程,称之为模板实例化(Template Instantiate)。实例化的语法是:
看几个例子: vector<int>
ClassA<double>
template <typename T0, typename T1> class ClassB
{
// Class body ...
};
ClassB<int, float> 当然,在实例化过程中,被绑定到模板参数上的类型(即模板实参)需要与模板形参正确匹配。 就如同函数一样,如果没有提供足够并匹配的参数,模板便不能正确的实例化。 1.1.3 模板类的成员函数定义由于C++11正式废弃“模板导出”这一特性,因此在模板类的变量在调用成员函数的时候,需要看到完整的成员函数定义。因此现在的模板类中的成员函数,通常都是以内联的方式实现。 例如: template <typename T>
class vector
{
public:
void clear()
{
// Function body
}
private:
T* elements;
}; 当然,我们也可以将 template <typename T>
class vector
{
public:
void clear(); // 注意这里只有声明
private:
T* elements;
};
template <typename T>
void vector<T>::clear() // 函数的实现放在这里
{
// Function body
} 函数的实现部分看起来略微拗口。我第一次学到的时候,觉得 void vector::clear()
{
// Function body
} 这样不就行了吗?但是简单想就会知道, 因此,在成员函数实现的时候,必须要提供模板参数。此外,为什么类型名不是 综上,正确的成员函数实现如下所示: template <typename T> // 模板参数
void vector<T> /*看起来像偏特化*/ ::clear() // 函数的实现放在这里
{
// Function body
} 1.2 Template Function的基本语法1.2.1 Template Function的声明和定义模板函数的语法与模板类基本相同,也是以关键字 template <typename T> void foo(T const& v);
template <typename T> T foo();
template <typename T, typename U> U foo(T const&);
template <typename T> void foo()
{
T var;
// ...
} 无论是函数模板还是类模板,在实际代码中看起来都是“千变万化”的。这些“变化”,主要是因为类型被当做了参数,导致代码中可以变化的部分更多了。 归根结底,模板无外乎两点:
当然,这里的“可变”实际上在代码编译好后就固定下来了,可以称之为编译期的可变性。 这里多啰嗦一点,主要也是想告诉大家,模板其实是个很简单的东西。 下面这个例子,或许可以帮助大家解决以下两个问题:
在我遇到的朋友中,即便如此对他解释了模板,即便他了解了模板,也仍然会对模板产生畏难情绪。毕竟从形式上来说,模板类和模板函数都要较非模板的版本更加复杂,阅读代码所需要理解的内容也有所增多。 如何才能克服这一问题,最终视模板如平坦代码呢? 答案只有一个:无他,唯手熟尔。 在学习模板的时候,要反复做以下的思考和练习:
通过重复以上的练习,应该可以对模板的语法和含义都有所掌握。如果提出问题本身有困难,或许下面这个经典案例可以作为你思考的开始:
当然和“设计模式”一样,模板在实际应用中,也会有一些固定的需求和解决方案。比较常见的场景包括:泛型(最基本的用法)、通过类型获得相应的信息(型别萃取)、编译期间的计算、类型间的推导和变换(从一个类型变换成另外一个类型,比如boost::function)。这些本文在以后的章节中会陆续介绍。 1.2.2 模板函数的使用我们先来看一个简单的函数模板,两个数相加: template <typename T> T Add(T a, T b)
{
return a + b;
} 函数模板的调用格式是: 函数模板名 < 模板参数列表 > ( 参数 ) 例如,我们想对两个 int a = 5;
int b = 3;
int result = Add<int>(a, b); 这时我们等于拥有了一个新函数: int Add<int>(int a, int b) { return a + b; } 这时在另外一个偏远的程序角落,你也需要求和。而此时你的参数类型是 Add<float>(a, b); 一切看起来都很完美。但如果你具备程序员的最佳美德——懒惰——的话,你肯定会这样想,我在调用 int a = 5;
int b = 3;
int result = Add(a, b); 编译器会心领神会地将 int a = 5;
char b = 3;
int result = Add(a, b); 第一个参数
好吧,"ambiguous",这个提示再明确不过了。 不过,只要你别逼得编译器精神分裂的话,编译器其实是非常聪明的,它可以从很多的蛛丝马迹中,猜测到你真正的意图,有如下面的例子: template <typename T> class A {};
template <typename T> T foo( A<T> v );
A<int> v;
foo(v); // 它能准确地猜到 T 是 int. 咦,编译器居然绕过了A这个外套,猜到了 下面轮到你的练习时间了。你试着写了很多的例子,但是其中一个你还是犯了疑惑: float data[1024];
template <typename T> T GetValue(int i)
{
return static_cast<T>(data[i]);
}
float a = GetValue(0); // 出错了!
int b = GetValue(1); // 也出错了! 为什么会出错呢?你仔细想了想,原来编译器是没办法去根据返回值推断类型的。函数调用的时候,返回值被谁接受还不知道呢。如下修改后,就一切正常了: float a = GetValue<float>(0);
int b = GetValue<int>(1); 嗯,是不是so easy啊?嗯,你又信心满满的做了一个练习: 你要写一个模板函数叫 DstT dest = c_style_cast<DstT>(src); 根据调用形式你知道了,有 我们把手上得到的信息来拼一拼,就可以编写自己的函数模板了: template <typename SrcT, typename DstT> DstT c_style_cast(SrcT v)
{
return (DstT)(v);
}
int v = 0;
float i = c_style_cast<float>(v); 嗯,很Easy嘛!我们F6一下…咦!这是什么意思! error C2783: 'DstT _1_2_2::c_style_cast(SrcT)' : could not deduce template argument for 'DstT' 然后你仔细的比较了一下,然后发现 … 模板参数有两个,而参数里面能得到的只有 float i = c_style_cast<int, float>(v); 嗯,很顺利的通过了。难道C++不能支持让参数推导一部分模板参数吗? 当然是可以的。只不过在部分推导、部分指定的情况下,编译器对模板参数的顺序是有限制的:先写需要指定的模板参数,再把能推导出来的模板参数放在后面。 在这个例子中,能推导出来的是 template <typename DstT, typename SrcT> DstT c_style_cast(SrcT v) // 模板参数 DstT 需要人肉指定,放前面。
{
return (DstT)(v);
}
int v = 0;
float i = c_style_cast<float>(v); // 形象地说,DstT会先把你指定的参数吃掉,剩下的就交给编译器从函数参数列表中推导啦。 1.3 整型也可是Template参数模板参数除了类型外(包括基本类型、结构、类类型等),也可以是一个整型数(Integral Number)。这里的整型数比较宽泛,包括布尔型,不同位数、有无符号的整型,甚至包括指针。我们将整型的模板参数和类型作为模板参数来做一个对比: template <typename T> class TemplateWithType;
template <int V> class TemplateWithValue; 我想这个时候你也更能理解 按照C++ Template最初的想法,模板不就是为了提供一个类型安全、易于调试的宏吗?有类型就够了,为什么要引入整型参数呢?考虑宏,它除了代码替换,还有一个作用是作为常数出现。所以整型模板参数最基本的用途,也是定义一个常数。例如这段代码的作用: template <typename T, int Size> struct Array
{
T data[Size];
};
Array<int, 16> arr; 便相当于下面这段代码: class IntArrayWithSize16
{
int data[16]; // int 替换了 T, 16 替换了 Size
};
IntArrayWithSize16 arr; 其中有一点需要注意,因为模板的匹配是在编译的时候完成的,所以实例化模板的时候所使用的参数,也必须要在编译期就能确定。例如以下的例子编译器就会报错: template <int i> class A {};
void foo()
{
int x = 3;
A<5> a; // 正确!
A<x> b; // error C2971: '_1_3::A' : template parameter 'i' : 'x' : a local variable cannot be used as a non-type argument
} 因为x不是一个编译期常量,所以 嗯,这里我们再来写几个相对复杂的例子: template <int i> class A
{
public:
void foo(int)
{
}
};
template <uint8_t a, typename b, void* c> class B {};
template <bool, void (*a)()> class C {};
template <void (A<3>::*a)(int)> class D {};
template <int i> int Add(int a) // 当然也能用于函数模板
{
return a + i;
}
void foo()
{
A<5> a;
B<7, A<5>, nullptr> b; // 模板参数可以是一个无符号八位整数,可以是模板生成的类;可以是一个指针。
C<false, &foo> c; // 模板参数可以是一个bool类型的常量,甚至可以是一个函数指针。
D<&A<3>::foo> d; // 丧心病狂啊!它还能是一个成员函数指针!
int x = Add<3>(5); // x == 8。因为整型模板参数无法从函数参数获得,所以只能是手工指定啦。
}
template <float a> class E {}; // ERROR: 别闹!早说过只能是整数类型的啦! 当然,除了单纯的用作常数之外,整型参数还有一些其它的用途。这些“其它”用途最重要的一点是让类型也可以像整数一样运算。《Modern C++ Design》给我们展示了很多这方面的例子。不过你不用急着去阅读那本天书,我们会在做好足够的知识铺垫后,让你轻松学会这些招数。 |