您现在的位置是:首页 >技术杂谈 >C++【模板进阶】网站首页技术杂谈
C++【模板进阶】
✨个人主页: 北 海
?所属专栏: C++修行之路
?操作环境: Visual Studio 2019 版本 16.11.17
文章目录
?前言
模板是搭建 STL
的基本工具,同时也是泛型编程思想的代表,模板用好了可以提高程序的灵活性,以便进行更高效的迭代开发,模板除了最基本的类型替换功能外,还有更多高阶操作:非类型模板参数、全特化、偏特化等,以及关于模板声明与定义不能分离(在两个不同的文件中)的问题,都将在本文中进行介绍
?️正文
1、非类型模板参数
之前所使用的模板参数都是用来匹配不同的类型,如 int
、double
、Date
等,模板参数除了可以匹配类型外,还可以匹配常量(非类型),完成如数组、位图等结构的大小确定
1.1、使用方法
在定义模板参数时,不再使用 class
或 typename
,而是直接使用具体的类型,如 size_t
,此时称为 非类型模板参数
注:非类型模板参数必须为常量,即在编译阶段确定值
利用 非类型模板参数 定义一个大小可以自由调整的 整型数组 类
template<size_t N>
class arr
{
public:
int& operator[](size_t pos)
{
assert(pos >= 0 && pos < N);
return _arr[pos];
}
size_t size() const
{
return N;
}
private:
int _arr[N]; //创建大小为 N 的整型数组
};
再加入一个模板参数:类型,此时就可以得到一个 泛型、大小可自定义 的数组
template<class T, size_t N>
class arr
{
public:
T& operator[](size_t pos)
{
assert(pos >= 0 && pos < N);
return _arr[pos];
}
size_t size() const
{
return N;
}
private:
T _arr[N]; //创建大小为 N 的整型数组
};
非类型模板参数支持缺省,因此写成这样也是合法的
template<class T, size_t N = 10> //缺省大小为10
1.2、类型要求
非类型模板参数要求类型为 整型家族,其他类型是不行的
比如下面这些 非类型模板参数 都是标准之内的
//整型家族(部分)
template<class T, int N>
class arr1 { /*……*/ };
template<class T, long N>
class arr2 { /*……*/ };
template<class T, char N>
class arr3 { /*……*/ };
而一旦使用其他家族类型作为 非类型模板参数,就会引发报错
//浮点型,非标准
template<class T, double N>
class arr4 { /*……*/ };
因此可以总结出,非类型模板参数 的使用要求为
- 只能将 整型家族 类型作为非类型模板参数,其他类型不在标准之内
- 非类型模板参数必须为常量(不可被修改),且需要在编译阶段确定结果
整型家族:
char
、short
、bool
、int
、long
、long long
等
1.3、实际例子:array
在 C++11
标准中,引入了一个新容器 array
,它就使用了 非类型模板参数,为一个真正意义上的 泛型数组,这个数组是用来对标传统数组的
注意: 部分老编译器可能不支持使用此容器
array
的第二个模板参数就是 非类型模板参数
#include <iostream>
#include <cassert>
#include <array>
using namespace std;
int main()
{
int arrOld[10] = { 0 }; //传统数组
array<int, 10> arrNew; //新标准中的数组
//与传统数组一样,新数组并没有进行初始化
//新数组对于越界读、写检查更为严格
arrOld[15]; //老数组越界读,未报错
arrNew[15]; //新数组则会报错
arrOld[12] = 0; //老数组越界写,不报错,出现严重的内存问题
arrNew[12] = 10; //新数组严格检查
return 0;
}
array
是泛型编程思想中的产物,支持了许多 STL
容器的功能,比如 迭代器 和 运算符重载 等实用功能,最主要的改进是 严格检查越界行为
实际开发中,很少使用
array
,因为它对标传统数组,连初始化都没有,vector
在功能和实用性上可以全面碾压,并且array
使用的是栈区
上的空间,存在栈溢出问题,可以说array
是一个鸡肋的容器
array
如何做到严格的全面检查?
- 这个很简单,得益于类的封装,在进行下标相关操作前,先将传入的下标
pos
进行合法性检验即可,如assert(pos >= 0 && pos < N)
2、模板特化
模板除了可以根据传入的类型进行实例化外,还可以指定实例化,这就好比普通汽车只能在公路上行驶,但我们也可以将车进行特殊改装,让其能在山川泥潭中驰骋;模板特化的用意就在于此,通过对 泛型思想的特殊化处理 ,更好的符合我们的使用需求
2.1、概念
通常情况下,模板可以帮我们实现一些与类型无关的代码,但在某些场景中,【泛型】无法满足调用方的精准需求,此时会引发错误,比如使用 日期类对象指针 构建优先级队列后,若不编写对应的仿函数,则比较结果会变为未定义
原因:泛型思想无法满足特殊场景
解决方案:利用模板的特化制定更加精准的比较逻辑
综上所述,所谓模板的特化,就是在原模板的基础之上,对原模板进行特殊化处理,创造出另一个 “特殊” 的模板,完成需求
2.2、函数模板特化
函数也可以使用模板,因此支持 模板的特化
比如在下面这个比较函数中,假若不进行特化,则会出现错误的结果
template<class T>
bool isEqual(T x, T y)
{
return x == y;
}
int main()
{
int x = 10;
int y = 20;
cout << "x == y: " << isEqual(x, y) << endl;
char str1[] = "Haha";
char str2[] = "Haha";
//此时泛型比的是地址,实际内容是相等的!
cout << "str1 == str2: " << isEqual(str1, str2) << endl;
return 0;
}
原因:字符串比较时,比较的是地址,而非内容
解决方案:利用模板的特化,为字符串的比较构建一个特殊模板
//函数模板特殊,专为 char* 服务
template<>
bool isEqual<char*>(char* x, char* y)
{
return strcmp(x, y) == 0;
}
此时比较的结果正常,成功解决了问题
不过对于函数模板特化来说,存在一个更加方便的东西:函数重载,同样也能解决特殊需求,同时写法没这么怪,不过既然存在 函数模板特化 这个语法,那么我们还是得学习下的
2.3、类模板特化
模板特化主要用在类模板中,它可以在泛型思想之上解决大部分特殊问题,并且类模板特化还可以分为:全特化和偏特化,适用于不同场景
后续举例时需要用到 Date
日期类,这里先把代码放出来
class Date
{
public:
Date(int year = 1970, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
bool operator<(const Date& d)const
{
return (_year < d._year) ||
(_year == d._year && _month < d._month) ||
(_year == d._year && _month == d._month && _day < d._day);
}
bool operator>(const Date& d)const
{
return (_year > d._year) ||
(_year == d._year && _month > d._month) ||
(_year == d._year && _month == d._month && _day > d._day);
}
private:
int _year;
int _month;
int _day;
};
2.3.1、全特化
全特化指 将所有的模板参数特化为具体类型,将模板全特化后,调用时,会优先选择更为匹配的模板类
//原模板
template<class T1, class T2>
class Test
{
public:
Test(const T1& t1, const T2& t2)
:_t1(t1)
,_t2(t2)
{
cout << "template<class T1, class T2>" << endl;
}
private:
T1 _t1;
T2 _t2;
};
//全特化后的模板
template<>
class Test<int, char>
{
public:
Test(const int& t1, const char& t2)
:_t1(t1)
, _t2(t2)
{
cout << "template<>" << endl;
}
private:
int _t1;
char _t2;
};
int main()
{
Test<int, int> T1(1, 2);
Test<int, char> T2(20, 'c');
return 0;
}
对模板进行全特化处理后,实际调用时,会优先选择已经特化并且类型符合的模板,这就好比虽然你家冰箱里有菜,但你还是想点外卖,因为外卖对于你来说更加合适
可以使用全特化,解决之前优先级队列中,类型为 日期类指针 Date*
的比较问题
注:这里只是举例说明,完整代码参考优先级队列相关文章
//对比较的仿函数进行全特化处理
template<>
struct less<Date*>
{
//比较 是否小于
bool operator()(Date* x, Date* y)
{
return *x < *y;
}
};
template<>
struct greater<Date*>
{
//比较 是否大于
bool operator()(Date* x, Date* y)
{
return *x > *y;
}
};
注意:
- 在进行全特化前,需要存在最基本的泛型模板
- 全特化模板中的模板参数可以不用写
- 需要在类名之后,指明具体的参数类型,否则无法实例化出对象
2.3.2、偏特化
偏特化,指 将泛型范围进一步限制,可以限制为某种类型的指针,也可以限制为具体类型
//原模板---两个模板参数
template<class T1, class T2>
class Test
{
public:
Test()
{
cout << "class Test" << endl;
}
};
//偏特化之一:限制为某种类型
template<class T>
class Test<T, int>
{
public:
Test()
{
cout << "class Test<T, int>" << endl;
}
};
//偏特化之二:限制为不同的具体类型
template<class T>
class Test<T*, T*>
{
public:
Test()
{
cout << "class Test<T*, T*>" << endl;
}
};
int main()
{
Test<double, double> t1;
Test<char, int> t2;
Test<Date*, Date*> t3;
return 0;
}
偏特化(尤其是限制为某种类型)在 泛型思想 和 特殊情况 之间做了折中处理,使得 限制范围式的偏特化 也可以实现 泛型
- 比如偏特化为
T*
,那么传int*
、char*
、Date*
都是可行的
借助偏特化解决指针无法正常比较问题(也是可以偏特化为引用类型的)
//原来的比较模板
template<class T>
class Less
{
public:
bool operator()(T x, T y) const
{
return x < y;
}
};
//偏特化后的比较模板
template<class T>
class Less<T*>
{
public:
bool operator()(T* x, T* y) const
{
return *x < *y;
}
};
int main()
{
Date d1 = { 2018, 4, 10 };
Date d2 = { 2023, 5, 10 };
cout << "d1 < d2: " << Less<Date>()(d1, d2) << endl;
cout << "&d1 < &d2: " << Less<Date*>()(&d1, &d2) << endl;
int a = 1;
int b = 2;
cout << "&a < &b: " << Less<int*>()(&a, &b) << endl;
return 0;
}
当然也可以使用 偏特化 解决 Date*
的比较问题,这里就不再演示
注意:
- 在进行偏特化前,需要存在最基本的泛型模板
- 偏特化与全特化很像,注意区分
3、模板的分离编译问题
早在 模板初阶 中,我们就已经知道了 模板不能进行分离编译,会引发链接问题
下面就来谈谈为什么会出现这个问题
3.1、失败原因
声明与定义分离后,在进行链接时,无法在符号表中找到目标地址进行跳转,因此链接错误
下面是 模板声明与定义写在同一个文件中时,具体的汇编代码执行步骤
Test.h
#pragma once
//声明
template<class T>
T add(const T x, const T y);
//定义
template<class T>
T add(const T x, const T y)
{
return x + y;
}
main.cpp
#include <iostream>
#include "Test.h"
using namespace std;
int main()
{
add(1, 2);
return 0;
}
声明与定义在同一个文件中时,可以直接找到函数的地址
代码从文本变为可执行程序所需要的步骤:
- 预处理:头文件展开、宏替换、条件编译、删除注释,生成纯净的C代码
- 编译:语法 / 词法 / 语义 分析、符号汇总,生成汇编代码
- 汇编:生成符号表,生成二进制指令
- 链接:合并段表,将符号表进行合并和重定位,生成可执行程序
当模板的 声明 与 定义 分离时,因为是 【泛型】,所以编译器无法确定函数原型,即 无法生成函数,也就无法获得函数地址,在符号表中进行函数链接时,必然失败
简单举个例子:抛开模板这个东西,在头文件中声明函数,但不定义,调用函数时,报的就是链接错误
Test.h
#pragma once
//只声明,不定义
void sub(int x, int y);
main.cpp
#include <iostream>
#include "Test.h"
using namespace std;
int main()
{
//add(1, 2);
sub(2, 1);
return 0;
}
3.2、解决方法
解决方法有两种:
- 在函数定义时进行模板特化,编译时生成地址以进行链接
- 模板的声明和定义不要分离,直接写在同一个文件中
//定义
//解决方法一:模板特化(不推荐,如果类型多的话,需要特化很多份)
template<>
int add(const int x, const int y)
{
return x + y;
}
//定义
//解决方法二:声明和定义写在同一个文件中
template<class T>
T add(const T x, const T y)
{
return x + y;
}
这也就解释了为什么涉及 模板 的类,其中的函数声明和定义会写在同一个文件中 (.h
),著名的 STL
库中的代码的声明和定义都是在一个 .h
文件中
为了让别人一眼就看出来头文件中包含了 声明 与 定义,可以将头文件后缀改为
.hpp
,著名的Boost
库中就有这样的命名方式
4、模板小结
模板是 STL
的基础支撑,假若没有模板、没有泛型编程思想,那么恐怕 "STL"
会变得非常大
模板的优点
- 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
- 增强了代码的灵活性
模板的缺点
- 模板会导致代码膨胀问题,也会导致编译时间变长
- 出现模板编译错误时,错误信息非常凌乱,不易定位错误
总之,模板 是一把双刃剑,既有优点,也有缺点,只有把它用好了,才能使代码 更灵活、更优雅
?总结
以上就是有关 C++【模板进阶】的全部内容了,在本文中,我们学习了非类型模板参数,认识了 C++11
中的新容器 array
;然后学习了模板的特化,见识了模板特化的各种场景;最后明白了模板声明与定义不能分离的根本原因,总之,模板很强,但想要用好还得多练
C++
初阶系列文章到此就正式结束了,后续将会继续更新 C++
进阶内容,比如 继承
、多态
、高阶二叉树
等等高能知识点,敬请期待吧
相关文章推荐
STL 之 适配器
C++ STL学习之【优先级队列】
C++ STL学习之【反向迭代器】
C++ STL学习之【容器适配器】 ===============
STL 之 list
C++ STL学习之【list的模拟实现】
C++ STL学习之【list的使用】 ===============
STL 之 vector
C++ STL学习之【vector的模拟实现】
C++ STL学习之【vector的使用】