CppTemplateTutorial
中文的C++ Template的教学指南。与知名书籍C++ Templates不同,该系列教程将C++ Templates作为一门图灵完备的语言来讲授,以求帮助读者对Meta-Programming融会贯通。(正在施工中)
Install / Use
/learn @wuye9036/CppTemplateTutorialREADME
C++ Template 进阶指南 <!-- omit in toc -->
章节目录由VSCode插件Markdown All in One生成。
1. 前言
1.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方面的集大成之作。它详细阐述了模板的语法、提供了和模板有关的语言细节信息,举了很多有代表性例子。但是对于模板新手来说,这本书细节如此丰富,让他们随随便便就打了退堂鼓缴械投降。
本文的写作初衷,就是通过“编程语言”的视角,介绍一个简单、清晰的“模板语言”。我会尽可能地将模板的诸多要素连串起来,用一些简单的例子帮助读者学习这门“语言”,让读者在编写、阅读模板代码的时候,能像 if(exp) { dosomething(); }一样的信手拈来,让“模板元编程”技术成为读者牢固掌握、可举一反三的有用技能。
1.2. 适宜读者群
因为本文并不是用于C++入门,例子中也多少会牵涉一些其它知识,因此如果读者能够具备以下条件,会读起来更加轻松:
- 熟悉C++的基本语法;
- 使用过STL;
- 熟悉一些常用的算法,以及递归等程序设计方法。
此外,尽管第一章会介绍一些Template的基本语法,但是还是会略显单薄。因此也希望读者能对C++ Template最基本语法形式有所了解和掌握;如果会编写基本的函数模板和类模板那就更好了。
诚如上节所述,本文并不是《C++ Templates》的简单重复,与《Modern C++ Design》交叠更少。从知识结构上,我建议大家可以先读本文,再阅读《C++ Templates》获取更丰富的语法与实现细节,以更进一步;《Modern C++ Design》除了元编程之外,还有很多的泛型编程示例,原则上泛型编程的部分与我所述的内容交叉不大,读者在读完1-3章了解模板的基本规则之后便可阅读《MCD》的相应章节;元编程部分(如Typelist)建议在阅读完本文之后再行阅读,或许会更易理解。
1.3. 版权
本文是随写随即同步到Github上,因此在行文中难免会遗漏引用。本文绝大部分内容应是直接承出我笔,但是也不定会有他山之石。所有指涉内容我会尽量以引号框记,或在上下文和边角注记中标示,如有遗漏烦请不吝指出。
全文所有为我所撰写的部分,作者均保留所有版权。如果有需要转帖或引用,还请注明出处并告知于我。
1.4. 推荐编译环境
C++编译器众多,且对模板的支持可能存在细微差别。如果没有特别强调,本书行文过程中,使用了下列编译器来测试文中提供的代码和示例:
- Clang 14.0.3; 15.0 (amd64)
- Visual Studio 2022 19.2+ (amd64)
此外,部分复杂实例我们还在文中提供了在线的编译器预览以方便大家阅读和测试。在线编译器参见: gcc.godbolt.org。
一些示例中用到的特性所对应的C++标准:
|特性|标准| |---|---| | std::decay_t<T> | C++ 14 |
1.5. 体例
1.5.1. 示例代码
void SampleCode() {
// 这是一段示例代码
}
1.5.2. 引用
引用自C++标准:
1.1.2/1 这是一段引用或翻译自标准的文字
引用自其他图书:
《书名》 这是一段引用或翻译自其他图书的文字
1.6. 意见、建议、喷、补遗、写作计划
- 需增加:
- 模板的使用动机。
- 增加“如何使用本文”一节。本节将说明全书的体例(强调字体、提示语、例子的组织),所有的描述、举例、引用在重审时将按照体例要求重新组织。
- 除了用于描述语法的例子外,其他例子将尽量赋予实际意义,以方便阐述意图。
- 在合适的章节完整叙述模板的类型推导规则。Parameter-Argument, auto variable, decltype, decltype(auto)
- 在函数模板重载和实例化的部分讲述ADL。
- 变参模板处应当按照标准(Argument Packing/Unpacking)来讲解。
- 建议:
- 比较模板和函数的差异性
- 蓝色:C++14 Return type deduction for normal functions 的分析
2. Template的基本语法
2.1. 什么是模板(Template)
2.2. 类模板 (Class Template) 的基本语法
2.2.1. “模板类”还是“类模板”
2.2.2. Class Template的与成员变量定义
我们来回顾一下最基本的Class Template声明和定义形式:
Class Template声明:
template <typename T> class ClassA;
Class Template定义:
template <typename T> class ClassA
{
T member;
};
template 是C++关键字,意味着我们接下来将定义一个模板。和函数一样,模板也有一系列参数。这些参数都被囊括在template之后的< >中。在上文的例子中, typename T便是模板参数。回顾一下与之相似的函数参数的声明形式:
void foo(int a);
T则可以类比为函数形参a,这里的“模板形参”T,也同函数形参一样取成任何你想要的名字;typename则类似于例子中函数参数类型int,它表示模板参数中的T将匹配一个类型。除了 typename 之外,我们在后面还要讲到,整型也可以作为模板的参数。
在定义完模板参数之后,便可以定义你所需要的类。不过在定义类的时候,除了一般类可以使用的类型外,你还可以使用在模板参数中使用的类型 T。可以说,这个 T是模板的精髓,因为你可以通过指定模板实参,将T替换成你所需要的类型。
例如我们用ClassA<int>来实例化类模板ClassA,那么ClassA<int>可以等同于以下的定义:
// 注意:这并不是有效的C++语法,只是为了说明模板的作用
typedef class {
int member;
} ClassA<int>;
可以看出,通过模板参数替换类型,可以获得很多形式相同的新类型,有效减少了代码量。这种用法,我们称之为“泛型”(Generic Programming),它最常见的应用,即是STL中的容器类模板。
2.2.3. 模板的使用
对于C++来说,类型最重要的作用之一就是用它去产生一个变量。例如我们定义了一个动态数组(列表)的类模板vector,它对于任意的元素类型都具有push_back和clear的操作,我们便可以如下定义这个类:
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<int>将int绑定到类模板vector上,获得了一个“普通的类vector<int>”;第二步通过“vector<int>”定义了一个变量。
与“普通的类”不同,类模板是不能直接用来定义变量的 —— 毕竟它的名字是“模板”而不是“类”。例如:
vector unknownVector; // 错误示例
这样就是错误的。我们把通过类型绑定将类模板变成“普通的类”的过程,称之为模板实例化(Template Instantiate)。实例化的语法是:
模板名 < [模板实参1,模板实参2,...] >
看几个例子:
vector<int>
ClassA<double>
template <typename T0, typename T1> class ClassB
{
// Class body ...
};
ClassB<int, float>
当然,在实例化过程中,被绑定到模板参数上的类型(即模板实参)需要与模板形参正确匹配。 就如同函数一样,如果没有提供足够并匹配的参数,模板便不能正确的实例化。
2.2.4. 类模板的成员函数定义
由于C++11正式废弃“模板导出”这一特性,因此在类模板的变量在调用成员函数的时候,需要看到完整的成员函数定义。因此现在的类模板中的成员函数,通常都是以内联的方式实现。 例如:
template <typename T>
class vector
{
public:
void clear()
{
// Function body
}
private:
T* elements;
};
当然,我们也可以将vector<T>::clear的定义部分放在类型之外,只不过这个时候的语法就显得蹩脚许多:
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
}
这样不就行了吗?但是简单想就会知道,clear里面是找不到泛型类型T的符号的。
因此,在成员函数实现的时候,必须要提供模板参数。此外,为什么类型名不是vector而是vector<T>呢?
如果你了解过模板的偏特化与特化的语法,应该能看出,这里的vector<T>在语法上类似于特化/偏特化。实际上,这里的函数定义也确实是成员函数的偏特化。特化和偏特化的概念,本文会在第二部分详细介绍。
综上,正确的成员函数实现如下所示:
template <typename T> // 模板参数
void vector<T> /*看起来像偏特化*/ ::clear() // 函数的实现放在这里
{
// Function body
}
2.3. 函数模板 (Function Template) 入门
2.3.1. 函数模板的声明和定义
函数模板的语法与类模板基本相同,也是以关键字template和模板参数列表作为声明与定义的开始。模板参数列表中的类型,可以出现在参数、返回值以及函数体中。比方说下面几个例子
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;
// ...
}
无论是函数模板还是类模板,在实际代码中看起来都是“千变万化”的。这些“变化”,主要是因为类型被当做了参数,导致代码中可以变化的部分更多了。
归根结底,模板无外乎两点:
-
函数或者类里面,有一些类型我们希望它能变化一下,我们用标识符来代替它,这就是“模板参数”;
-
在需要这些类型的地方,写上相对应的标识符(“模板参数”)。
当然,这里的“可变”实际上在代码编译好后就固定下来了,可以称之为编译期的可变性。
这里多啰嗦一点,主要也是想告诉大家,模板其实是个很简单的东西。
下面这个例子,或许可以帮助大家解决以下两个问题:
-
什么样的需求会使用模板来解决?
-
怎样把脑海中的“泛型”变成真正“泛型”的代码?
举个例子:generic typed function ‘add’
在我遇到的朋友中,即便如此对他解释了模板,即便他了解了模板,也仍然会对模板产生畏难情绪。毕竟从形式上来说,模板化的类和模板化的函数都要较非模板的版本更加复杂,阅读代码所需要理解的内容也有所增多。
如何才能克服这一问题,最终视模板如平坦代码呢?
答案只有一个:无他,唯手熟尔。
在学习模板的时候,要反复做以下的思考和练习:
-
提出问题:我的需求能不能用模板来解决?
-
怎么解决?
-
把解决方案用代码写出来。
-
如果失败了,找到原因。是知识有盲点(例如不知道怎么将
T&转化成T),还是不可行(比如试图利用浮点常量特化类模板,但实际上这样做是不可行的)?
通过重复以上的练习,应该可以对模板的语法和含义都有所掌握。如果提出问题本身有困难,或许下面这个经典案例可以作为你思考的开始:
-
写一个泛型的数据结构:例如,线性表,数组,链表,二叉树;
-
写一个可以在不同数据结构、不同的元素类型上工作的泛型函数,例如求和;
当然和“设计模式”一样,模板在实际应用中,也会有一些固定的需求和解决方案。比较常见的场景包括:泛型(最基本的用法)、通过类型获得相应的信息(型别萃取)、编译期间的计算、类型间的推导和变换(从一个类型变换成另外一个类型,比如boost::function)。这些本文在以后的章节中会陆续介绍。
2.3.2. 函数模板的使用
我们先来看一个简单的函数模板,两个数相加:
template <typename T> T Add(T a, T b)
{
return a + b;
}
函数模板的调用格式是:
函数模板名 < 模板参数列表 > ( 参数 )
例如,我们想对两个 int 求和,那么套用类的模板实例化方法,我们可以这么写:
int a = 5;
int b = 3;
int result = Add<int>(a, b);
这时我们等于拥有了一个新函数:
int Add<int>(int a, int b) { return a + b; }
这时在另外一个偏远的程序角落,你也需要求和。而此时你的参数类型是 float ,于是你写下:
Add<float>(a, b);
一切看起来都很完美。但如果你具备程序员的最佳美德——懒惰——的话,你肯定会这样想,我在调用 Add<int>(a, b) 的时候, a 和 b 匹配的都是那个 T。编译器就应该知道那个 T 实际上是 int 呀?为什么还要我多此一举写 Add<int> 呢?
唔,我想说的是,编译器的作者也是这么想的。所以实际上你在编译器里面写下以下片段:
int a = 5;
int b = 3;
int result = Add(a, b);
编译器会心领神会地将 Add 变成 Add<int>。但是编译器不能面对模棱两可的答案。比如你这么写的话呢?
int a = 5;
char b = 3;
int result = Add(a, b);
第一个参数 a 告诉编译器,这个 T 是 int。编译器点点头说,好。但是第二个参数 b 不高兴了,告诉编译器说,
Related Skills
node-connect
338.0kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
83.4kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
338.0kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
commit-push-pr
83.4kCommit, push, and open a PR
