C++编程学习指导|C++初阶(模板进阶)

泛型编程和模板 STL 简介
STL(standard template libaray)是 C++ 标准库的重要组成部分:标准模板库。STL 不仅是一个可复用的组件库,而且
是一个包罗数据结构与算法的软件框架。
原始版本
Alexander Stepanov、Meng Lee 在惠普实验室完成的原始版本,本着开源精神,他们声明允许任何人任意运用、拷贝、修改、传播、商业使用这些代码,无需付费。唯一的条件就是也需要向原始版本一样做开源使用。也成为 HP 版本,是所有STL实现版本的始祖。
P. J. 版本
由 P. J. Plauger 开发,继承自 HP 版本,被 Windows Visual C++ 采用,不能公开或修改,缺陷:可读性比较低,符号命名比较怪异。
RW版本
由 Rouge Wage 公司开发,继承自 HP 版本,被 C++ Builder 采用,不能公开或修改,可读性一般。
SGI版本
由 Silicon Graphics Computer Systems,Inc 公司开发,继承自 HP 版 本。被 GCC(Linux) 采用,可移植性好,可公开、修改甚至贩卖,从命名风格和编程风格上看,阅读性非常高。学习 STL 要阅读部分源代码,主要参考的就是这个版本。
C++编程学习指导|C++初阶(模板进阶)
文章图片

网上有句话说:“不懂 STL,不要说你会 C++ ”。STL 是 C++ 中的优秀作品,有了它的陪伴,许多底层的数据结构以及算法都不需要自己重新造轮子,站在前人的肩膀上,健步如飞的快速开发。
STL 的缺陷

  1. STL库的更新太慢了。这个得严重吐槽,上一版靠谱是C++98,中间的C++03基本一些修订。C++11出来已经相隔了13年,STL才进一步更新。
  2. STL现在都没有支持线程安全。并发环境下需要我们自己加锁。且锁的粒度是比较大的。
  3. STL极度的追求效率,导致内部比较复杂。比如类型萃取,迭代器萃取。
  4. STL的使用会有代码膨胀的问题,比如使用vector/vector/vector这样会生成多份代码,当然这是模板语法本身导致的。
1. 模板初阶 1.1 泛型编程
void Swap(int& left, int& right) { // 交换两个整型变量 int tmp = left; left = right; right = tmp; } void Swap(double& left, double& right) { // 交换两个浮点型变量 double tmp = left; //... } void Swap(char*& left, char*& right) { // 交换两个指针变量 char* tmp = left; //... }

如上述代码所示,代码模块的功能非常相似,只是处理的数据类型不同,为每种类型,都写出对应的代码,这样代码复用率太低,且可维护性低。
可以在函数中传入通用的数据类型,从而合并代码,泛型的出现就是专门解决这个问题的。泛型编程的定义就是:编写与类型无关的代码,是一种代码复用的手段。
屏蔽掉数据和操作数据的细节,让算法更为通用,让编程者更多地关注算法的结构,而不是在算法中处理不同的数据类型。编程语言本质上帮助程序员屏蔽底层机器代码的实现,而让我们可以更为关注业务逻辑代码。
泛型编程在 C++ 中,就体现在模板上。可以用模板技术来抽象类型,这样可以写出类型无关的代码。模板是泛型编程的基础,模板是创建泛型类或函数的蓝图或公式。模板分为函数模板和类模板。
1.2 函数模板
函数模板的定义格式 函数模板代表了一个函数家族,该函数模板与类型无关,类型在使用时被参数化,根据实参类型产生函数的特定函数版本。
//1. template 返回类型 函数名 (参数列表) {} //2. template 返回类型 函数名 (参数列表) {}

如上两种方式都可以,typename 和 class 都是用来定义模板关键字的。
为解决最开始的例子,交换两个变量的函数代码复用问题,可以使用如下的方式:
template void Swap(T& left, T& right) { T tmp = left; left = right; right = tmp; }

函数模板的原理 但当不同数据类型调用该函数模板时,使用的是同一个函数吗?
函数模板本是个蓝图,是编译器产生特定具体类型函数的模具,它本身算是模板而不是函数。模板将重复性的操作交给编译器去执行,产生了多个适用于不同类型的函数。
C++编程学习指导|C++初阶(模板进阶)
文章图片

参数类型不同,函数栈帧必然不同,那必然不会是同一个函数。vs2019 甚至多了这样的窗口:
C++编程学习指导|C++初阶(模板进阶)
文章图片

C++编程学习指导|C++初阶(模板进阶)
文章图片

函数地址不同,可见调用的不是同一个函数。在代码编译阶段,对于模板函数,编译器根据传入的实参类型也推演生成对应类型的函数以供调用。
函数模板的实例化 不同类型的参数使用该函数模板的过程,称为函数模板的实例化。模板参数实例化分为两种:隐式实例化和显式实例化。
  • 隐式实例化:编译器根据实参自动推演参数的实际类型的模板实例化过程。
  • 显式实例化:函数名后<>中指定模板参数的类型的实例化过程。
template T Add(const T& rx, const T& ry) { return rx + ry; } int main() { int a = 10, b = 20; double c = 3.33, d = 4.44; //1. 隐式实例化 Add(a, b); Add(c, d); //2. 显式实例化 Add(a, d); Add(a, d); Add(a, d); // Err:类型不一致 Add((double)a, d); // 提前强转参数类型 return 0; }

如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。
函数模板的匹配原则
int Add(const int& rx, const int& ry) // 适用于int类型 { return rx + ry; } template T Add(const T& rx, const T& ry) // 通用类型模板 { return rx + ry; }

  • 当存在和函数模板同名同参数列表的函数时,编译器会优先调用普通函数而非模板。
【C++编程学习指导|C++初阶(模板进阶)】存在普通函数说明开发者更想使用函数而非模板,调用函数也可以省去模板实例化的环节。
C++编程学习指导|C++初阶(模板进阶)
文章图片

  • 函数模板不允许自动类型转换,但普通函数传参时允许发生隐式类型转换。
1.3 类模板
使用typedef类型重命名不能够支持所有类型,且不能用同一个类创建不同类型成员的对象。只是将一个固定的类型重命名,并不属于泛型编程。
类模板的定义格式 定义格式和函数模板类似:
template class 类模板名 { //... };

template class Stack { public: Stack(int capacity = 4) :_top(0), _capacity(capacity) { _data = https://www.it610.com/article/new T[capacity]; } ~Stack() { delete[] _data; _data = nullptr; _top = _capacity = 0; } void Push(T x) { //.... } private: T* _data; int _top; int _capacity; };

类模板的实例化 类模板和函数模板的实例化有所不同,类模板实例化必须要在模板名后跟<>并指明类型。类模板是模板而不是类,实例化后生成的类才是真正的类。
//Stack是类模板名,Stack才是类型 Stack st1; Stack st2;

成员函数声明定义分离,函数体定义在类外时,不仅要声明函数所属类域,还要声明模板类型。
template // 声明模板类型 void Stack::Push(T x) { // 声明所属类域 _data[top++] = x; //... }


2. 模板进阶 2.1 非类型模板参数
模板的参数不一定是这样的虚拟类型参数,还有非类型的模板参数。
template //类型参数 class A; template > //非类型参数 class A;

比如实现一个静态的栈结构,限定数组的大小可以宏定义的方式,也可以采用定义常变量的方式,当然也可以使用非类型模板参数:
//#define N 100 template class Stack { //... private: T _a[N]; }; Stack st1; Stack st2;

C++并不推荐使用宏,因为宏的缺点很多。使用非类型模板参数可以灵活控制该常量的数值。
  • 非类型模板参数的类型只能是整型家族类型,也就是charshortintlong等。
  • 非类型模板参数只能是常量,和宏一样都是在编译阶段被替换成对应的常量。
  • 非类型模板参数支持给缺省值,和普通参数的缺省值一样只能从右到左给。

2.2 模板的特化
函数模板特化 通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能不能得到想要的结果,比如:
C++编程学习指导|C++初阶(模板进阶)
文章图片

T被实例化为基本类型时,能够完成任务,但是当T被实例化为指针类型时,需求并不是比较指针的大小而是比较指针所指向的对象的大小。
此时该模板就不能起作用,需要重载出一个版本专门针对对象指针类型的比较函数,特化该函数的参数。
template bool ObjLess(T l, T r) { return l < r; } template <> bool ObjLess(Date* l, Date* r) { //针对对象指针类型 return *l < *r; }

函数模板特化是从原来的函数模板特化出一个类型专有的版本,相当于针对一个类型的特殊解法。
当函数模板特化不如直接重载一个版本,模板特化在函数处显得比较鸡肋。
模板特化主要是用于类模板特化。
类模板特化 类模板特化和函数模板特化差不多,都是针对某些特殊类型进行特殊处理,用在类处就是特化类的成员变量的类型。
全特化 全特化是模板参数列表中所有模板参数都进行特化。
template class Data { public: Data() { cout << "Data" << endl; } private: T1 _a; T2 _c; }; template <> class Data { // 特化处理int,char类型 public: Data() { cout << "Data" << endl; } private: int _a; char _c; };

偏特化 只限制部分参数为固定类型,其他仍是模板参数类型。
template class Data //第二个模板参数为char { public: Data() { cout << "Data" << endl; } private: T1 _a; char _c; };

换言之,特化是对模板参数的进一步限制,如下代码所示,模板参数的指针和引用也算特化。
template class Data { public: Data() { cout << "Data" << endl; } private: T1 _a; char _c; }; template class Data { public: Data() { cout << "Data" << endl; } private: T1 _a; char _c; };

非类型模板参数也可以用来特化。
C++编程学习指导|C++初阶(模板进阶)
文章图片

2.3 模板分离编译
一般一个项目为方便维护,都是采用声明和定义分离的形式。.h文件放类的定义和函数的声明,.cpp文件放函数的具体声明。
当对模板进行声明和定义分离时,会出现找不当该函数的链接错误:
C++编程学习指导|C++初阶(模板进阶)
文章图片

test.cpp中有Func2()的声明,所以编译的时候没有报错,待链接时查找Func2的符号表发现并没有void Fun2(int x)这样可调用的函数。
因为模板都是在编译时实例化出对应的版本,但编译时(还没到链接)当前文件并没有该函数的调用,就没有生成对应的函数,故链接时报出没有可匹配的函数实例的错误。
Test.cpp中有函数调用但没有函数定义,等链接时到Func.cpp中查找函数,Func.cpp中有函数定义但没有调用,就没有生成对应的函数实例。
解决方法 声明和定义不要分离,都放到.h文件即可,编译时就能确定变量的地址。
其次,还有一种显式实例化的方法,在Func.cpp文件里显式实例化出对应需要的类型版本。但需要手动指定还是很麻烦的。
C++编程学习指导|C++初阶(模板进阶)
文章图片

3. 模板总结
优点
模板复用代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
增强了代码的灵活性
缺点
模板会导致代码膨胀问题,也会导致编译时间变长
出现模板编译错误时,错误信息非常凌乱,不易定位错误

    推荐阅读