C++模板进阶
模板
源自于泛型编程
的思想,是将算法抽象化编写,前面在C++模板初阶中讲解了模板的初阶用法,本文我们来解锁模板的进阶用法
1. 非类型模板参数
模板参数分为类型形参与非类型形参
- 类型形参:出现在模板参数列表中,跟在
class
或者typename
之类的参数类型名称 - 非类型形参,就是用一个
常量
作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用
下面利用非类型模板参数定义一个类型和大小可以自由调整的整型数组类
//非类型函数模板
template<class T, size_t N = 10>
class Array
{
private:
T arr[N];
};
int main()
{
Array<int, 20> a1; //int 20个数据
Array<double> a2; //double 默认10个数据
cout << typeid(a1).name() << endl;
cout << typeid(a2).name() << endl;
return 0;
}
==注意:==
- 非类型模板参数的类型只能是整型家族,其他类型都是不允许的
- 非类型的模板参数必须在编译期就能确认结果,所以参数必须为常量
2. 模板特化
2.1 概念引入
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理,比如:实现了一个专门用来进行小于比较的函数模板
//日期类
class Date
{
public:
Date(int year = 2003, int month = 4, int day = 12)
:_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);
}
friend ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "年" << d._month << "月" << d._day << "日";
return _cout;
}
private:
int _year;
int _month;
int _day;
};
//函数模板
template<class T>
bool Less(T a, T b)
{
return a < b;
}
int main()
{
cout << Less(3, 6) << endl; //结果正确
Date d1(2023, 4, 12);
Date d2(2023, 4, 13);
cout << Less(d1, d2) << endl; //结果正确
Date* p1 = &d1;
Date* p2 = &d2;
cout << Less(p1, p2) << endl; //结果错误
return 0;
}
可以看到,Less
绝对多数情况下都可以正常比较,但是在特殊场景下就得到错误的结果。上述示例中,p1
指向的d1
显然小于p2
指向的d2
对象,但是Less内部并没有比较p1
和p2
指向的对象内容,而比较的是p1
和p2
指针的地址,这就无法达到预期而错误。
此时,就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化与类模板特化
2.2 函数模板特化
函数模板的特化步骤:
必须要先有一个基础的函数模板
关键字
template
后面接一对空的尖括号<>
函数名后跟一对尖括号,尖括号中指定需要特化的类型
函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
拿改进上面的代码来举个例子
//构建日期类Date
//...
//函数模板
template<class T>
bool Less(T a, T b)
{
return a < b;
}
//模板特化
template<>
bool Less<Date*>(Date* a, Date* b)
{
return *a < *b;
}
int main()
{
cout << Less(3, 6) << endl;
Date d1(2023, 4, 12);
Date d2(2023, 4, 13);
cout << Less(d1, d2) << endl;
Date* p1 = &d1;
Date* p2 = &d2;
cout << Less(p1, p2) << endl;
return 0;
}
这样第三个指向内容比较,就会调用特化之后的版本,而不走模板生成了,从而达到正确结果
不过相对于函数模板特化,我们还学过一个更加方便的方式,那就是函数重载
,也可以在原错误代码中直接加上一段对应的函数重载,也能很好的解决问题
bool Less(Date* left, Date* right)
{
return *left < *right;
}
函数重载的实现更加方便,可读性更高,所以在一般情况下函数模板不建议特化
2.3 类模板特化
类模板特化分为:全特化和偏特化,针对不同的场景可以解决很多特殊问题
2.3.1 全特化
全特化是指将模板参数列表中所有的参数都确定化
//原类模板
template<class T1, class T2>
class Data
{
public:
Data()
{
cout << "Data<T1, T2>" << endl;
}
private:
T1 _d1;
T2 _d2;
};
//全特化--将模板参数列表中所有的参数都确定
template<>
class Data<int, char>
{
public:
Data()
{
cout << "Data<int, char>" << endl;
}
private:
int _d1;
char _d2;
};
int main()
{
Data<int, int> d1; //Data<T1, T2>
Data<int, char> d2; //Data<int, char>
return 0;
}
对模板全特化后,在实际调用时,会优先选择已经特化并且类型符合的模板
2.3.2 偏特化
偏特化是指针对任何模版参数进一步进行条件限制设计的特化版本
//偏特化--针对任何模板参数进一步进行条件限制设计特化版本
template<class T1, class T2>
class Data
{
public:
Data()
{
cout << "Data<T1, T2>" << endl;
}
private:
T1 _d1;
T2 _d2;
};
//部分特化
template<class T1>
//将第二个参数特化成int
class Data<T1, int>
{
public:
Data()
{
cout << "Data<T1, int>" << endl;
}
private:
T1 _d1;
int _d2;
};
//参数进一步限制
//两个参数特化成指针类型
template<typename T1, typename T2>
class Data <T1*, T2*>
{
public:
Data()
{
cout << "Data<T1*, T2*>" << endl;
}
private:
T1 _d1;
T2 _d2;
};
//两个参数特化成引用类型
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:
Data(const T1& d1, const T2& d2)
:_d1(d1)
, _d2(d2)
{
cout << "Data<T1&, T2&>" << endl;
}
private:
const T1& _d1;
const T2& _d2;
};
int main()
{
Data<int, char> d1; //Data<T1, T2>
Data<char, int> d2; //Data<T1, int>
Data<int*, char*> d3; //Data<T1*, T2*>
Data<int&, char&> d4(1, 2); //Data<T1&, T2&>
return 0;
}
3. 模板分离编译
3.1 分离编译的概念
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式
在模板初阶中我们就指明了模板不能进行分离编译,会引发链接问题,下面我们就来分析一下为什么会出现这个问题
3.2 场景讲解
先来演示一下模板的声明定义不分离时的汇编代码执行步骤
//Func.h文件
#pragma once
#include <iostream>
using namespace std;
template<class T>
T Sub(const T& left, const T& right);
template<class T>
T Sub(const T& left, const T& right)
{
return left - right;
}
//Test.cpp文件
#include <iostream>
#include "Func.h"
using namespace std;
int main()
{
Sub(6, 8); //模板声明定义不分离
return 0;
}
通过反汇编我么可以了解到,在调用类模板函数时,会先调用add
函数,根据声明时提供的地址来找到函数定义,再来执行函数并返回结果,而声明与定义在同一个文件中时,可以直接找到函数的地址
再来看看模板的定义声明分离时的情况
当模板的声明与定义分离后,因为是模板泛型,编译器无法确定其函数原型,所以无法生成函数,当然也无法获得函数地址,在符号表中进行函数链接时,就会导致链接错误
3.3 解决方法
这里有两种解决方法
第一种是将声明和定义放到一个文件
//声明和定义放到同一个文件中
template<class T>
T Sub(const T& left, const T& right);
template<class T>
T Sub(const T& left, const T& right)
{
return left - right;
}
第二种是模板定义的位置显式实例化,也就是模板特化
//模板特化
template<>
int Sub(const int x, const int y)
{
return x - y;
}
不过使用第二种方式的话,如果类型很多,就要对应的特化很多份,所以这种方法是不太推荐的,推荐第一种方法
4. 模板总结
模板是泛型思想的体现,在很多场景下都能帮助我们很好的优化代码
模板的优点:
模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
增强了代码的灵活性
模板的缺陷:
模板会导致代码膨胀问题,也会导致编译时间变长
出现模板编译错误时,错误信息非常凌乱,不易定位错误
C++模板进阶到这里就介绍结束了,本篇文章对你由帮助的话,期待大佬们的三连,你们的支持是我最大的动力!
文章有写的不足或是错误的地方,欢迎评论或私信指出,我会在第一时间改正