1. 非类型模板参数
模板参数分为 类类型形参 与 非类型形参。
类型形参即:出现在模板参数列表中,跟在 class 或者 typename 之类的参数类型名称。
非类型形参:就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。(即有点替代 宏定义的意思)
定义一个模板类型的静态数组
namespace my { // 定义一个模板类型的静态数组 template<class T, size_t N = 10> class array { // ..... private: T _array[N]; // 直接将 N 当作一个固定的常量使用 size_t _size; }; }
注意:
1. 浮点数、类对象以及字符串是不允许作为非类型模板参数的。
自定义类型也不行!!!
C++20之前,只允许整形做非类型模板参数 T
C++20之后,可以支持 double 等其他内置类型
2. 非类型的模板参数必须在编译期就能确认结果。
2. 模板的特化
模板的特化:针对某些特殊类型,会优先匹配 特化的模板,对其进行特殊处理
(特化感觉是一种设计理念,直接将某些想要特殊处理的类型拎出来单独处理)
2.1 概念与使用
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理
即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化与类模板特化。
举例:
比如:实现了一个专门用来进行日期类大小的函数模板
正常来说,应该传两个日期类对象进行比较,但是会出现传两个日期类对象的指针的情况,此时我们想要比较的是 指针指向的日期类对象,而不是 这两个指针的本身的大小
p1 和 p2 是两个对象的指针,若直接使用 Less函数模板进行比较,则比较的是两个指针本身
template<class T>
bool Less(T left, T right)
{
return left < right;
}
int main()
{
Date d1(2022, 7, 7);
Date d2(2022, 7, 8);
cout << Less(d1, d2) << endl; // 可以比较,结果正确
// 当传指针比较时:调用特化模板
Date* p1 = &d1;
Date* p2 = &d2;
cout << Less(p1, p2) << endl; // 可以比较,结果错误
return 0;
}
因此需要写一个特化版本:针对于指针类型的 特殊版本
当编译器识别到 传过来的是两个指针,会直接优先匹配下面这个 特化模板
// 特化:对于一些特殊类型 的 特殊化处理
// 当比较 两个日期类对象时,可能会传递过来两个指针类型,就直接走下面这段逻辑
template<>
bool Less<Date*>(Date* left, Date* right)
{
return *left < *right;
}
2.2 函数模板特化
函数模板的特化步骤:
1. 必须要先有一个基础的函数模板
2. 关键字template后面接一对空的尖括号<>
3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
2.2.1 讨论 特化模板的 const 修饰的位置
(注:下面这段代码和上面那段代码是一样的)
template<class T> bool Less(T left, T right) { return left < right; } // 特化:对于一些特殊类型 的 特殊化处理 // 当比较 两个日期类对象时,可能会传递过来两个指针类型,就直接走下面这段逻辑 template<> bool Less<Date*>(Date* left, Date* right) { return *left < *right; } int main() { Date d1(2022, 7, 7); Date d2(2022, 7, 8); cout << Less(d1, d2) << endl; // 可以比较,结果正确 // 当传指针比较时:调用特化模板 Date* p1 = &d1; Date* p2 = &d2; cout << Less(p1, p2) << endl; // 可以比较,结果错误 return 0; }
但是如果在函数模板处都加上 const 和 & 修饰 时,那个特化模板就会报错了
(本来就应该加上 const 和 & 修饰,模板不应该是传值传参)
(const T& left, const T& right)
template<class T> bool Less(const T& left, const T& right) { return left < right; }
专用化就是特殊化的意思,即那个特化模板 匹配不上它的主模版
此时可能会 想要给 特化模板也加上 const 和 & 修饰
template<> bool Less<Date*>(const Date*& left, const Date*& right) { return *left < *right; }
没用,照样报同样的错
why??
这里涉及 C++语法的细节理解
答:在我们的 函数模板(即主模板处) const 修饰的是 Left 和 Right (即类型 T 之后的 变量)
而 特化模板 的 const 修饰的是 *Left 和 * Right (而不是 类型 T* 之后的 Left 和 Right )
因此不匹配
解决办法:
1、修改特化模板的 const 的指向问题
template<> bool Less<Date*>(Date* const& left, Date* const& right) { return *left < *right; }
2、干脆直接不用 函数特化模板,直接实例化出来一个现成的函数
bool Less(Date* left, Date* right) { return *left < *right; }
当现成的函数 和 特化模板 同时存在时,编译器是会优先吃现成的,不吃半成品的模板
因此,一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出(即解决方法 2)。
直接写一个函数的方法:实现简单明了,代码的可读性高,容易书写,因为对于一些参数类型复杂的函数模板,特化时特别给 出,因此函数模板不建议特化。
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; } }; int main() { Data<int, int> d1; Data<int, char> d2; 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 Data<int, char> { public: Data() { cout << "Data<int, char> - 全特化" << endl; } }; // 偏特化/半特化:依旧保留着没有特化的类型 T1,而 int 是特化的类型,只要第二个类型是 int 就一定会优先匹配到下面这个 特化模板(第一个类型无所谓是 什么类型) template <class T1> class Data<T1, int> { public: Data() { cout << "Data<T1, int> - 偏特化" << endl; } private: T1 _d1; int _d2; }; int main() { Data<int, int> d1; Data<int, char> d2; Data<double, int>d3; cout << '\n'; return 0; }
打印结果
2.3.3 限定模版的类型(特殊的偏特化)
这个有点有趣
特性介绍:对参数更进一步的限制
偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。
限定模版的类型的 偏特化:相较于之前”原始“的全特化,这个更加 ”像模板“,使用更加便利
比如:我们要给指针类型 特化处理
// 类模板 template<class T1, class T2> class Data { public: Data() { cout << "Data<T1, T2> - 原模板" << endl; } private: T1 _d1; T2 _d2; }; // 给指针类型 特化处理 template<> class Data<int*, int*> { public: Data() { cout << "Data<int*, int*> - 全特化" << endl; } };
但是,这样的特化,只适用于 int 的指针类型,而处理不了其他类型的指针
因此,需要使用 限定模版的类型的偏特化
// 限定模版的类型 template <typename T1, typename T2> class Data <T1*, T2*> { public: Data() { cout << "Data<T1*, T2*>-偏特化" << endl << endl; } };
这个可以匹配所有类型的指针类型形式,有点模板那味
(不是针对某个指针,而是针对指针这个整一个大类)
还可以 限定引用 或 引用和* 混用
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:
Data()
{
cout << typeid(T1).name() << endl;
cout << typeid(T2).name() << endl;
cout << "Data<T1&, T2&>" << endl << endl;
}
private:
};
template <typename T1, typename T2>
class Data <T1&, T2*>
{
public:
Data()
{
cout << typeid(T1).name() << endl;
cout << typeid(T2).name() << endl;
cout << "Data<T1&, T2*>" << endl << endl;
}
private:
};
这个类型的匹配规则的优点介绍:
下面代码表现:在 d4 中 int* 匹配 T1* ,则 T1 是 int ,double* 匹配 T1* ,则 T1 是 double
这个类型的匹配规则的优点:使 T1 和 T1* 都可以使用
T1 x; T1* y;
如果 int* 匹配 T1* 使 T1 就是 int* ,则限制住我们只能使用 一种指针类型
// 限定模版的类型 template <typename T1, typename T2> class Data <T1*, T2*> { public: Data() { cout << typeid(T1).name() << endl; cout << typeid(T2).name() << endl; cout << "Data<T1*, T2*>-偏特化" << endl << endl; } }; int main() { Data<int*, double*> d4; Data<int*, int**> d5; return 0; }
3. 模板分离编译
3.1 什么是分离编译
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式
3.2 模板的分离编译
假如有以下场景,模板的声明与定义分离开,在头文件中进行声明,源文件中完成定义:
// a.h template<class T> T Add(const T& left, const T& right); // a.cpp template<class T> T Add(const T& left, const T& right) { return left + right; } // main.cpp #include"a.h" int main() { Add(1, 2); Add(1.0, 2.0); return 0; }
在编译阶段
在 a.cpp 中,编译器没有查找到 对 Add 模板的实例化,就不生成 具体的加法函数 Add
另外 模板本就不会被编译,而生成指令,因为模版没有被实例化
当然也就,没有Add的地址放进符号表,所以链接也就不会被找到
在 main.obj 中调用的 Add<int> 与 Add<double> 编译器在链接时才会找其地址,但是这两个函数没有实例化没有生成具体代码(因为 a.cpp 没有生成这个 Add函数),因此链接时报错
简单来说
- a.cpp 是有函数定义的地方,但不知道实例化出什么
- main.obj 是由函数声明的地方,但是没有函数定义(mian文件中包含了 a.h 会展开:有函数声明)
解决方法
1、将声明和定义放到同一个文件 "xxx.hpp" 里面 或者 xxx.h 。推荐使用这种。
之前分开不行是因为:在main文件中调用函数模板时知道实例化成什么,但没有定义,a.cpp 定义的地方却不知道实例化成什么
而将 声明定义 放在同一个文件就可以解决这个问题
2、模板定义的位置显式实例化。这种方法不实用,不推荐使用。
表示直接告诉编译器这里要怎么实例化
即在 模板定义的 a.cpp 文件中,显式实例化需要的 模板出来,但是这种方式较为被动 ,不方便,不能适用于所有类型,需要不断的添加显式实例化(你这时候需要 int,改天你又需要 double 类型呢?还要写一次)
4. 模板总结
【优点】
1. 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL) 因此而产生
2. 增强了代码的灵活性
【缺陷】
1. 模板会导致代码膨胀问题,也会导致编译时间变长
2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误