🔥个人主页: Forcible Bug Maker
🔥专栏:C++
目录
- 🌈前言
- 🔥非类型模板参数
- 🔥模板的特化
- ==概念==
- ==函数模板特化==
- ==类模板特化==
- 全特化
- 偏特化
- 🔥模板分离编译
- ==为什么不建议声明和定义分离?==
- ==解决方案==
- ==总结==
- 🌈结语
🌈前言
本篇博客主要内容:对模板初阶内容的补充,深入对模板的了解和使用。
模板初阶当中,我已经介绍过了。什么是泛型编程;模板,分为函数模板和类模板,以及它们的基本用法。在本篇博客中,我们讲补充模板的一些特殊用法,并填补之前挖的坑,为什么不建议模板进行分离编译(声明和定义放到两个文件中)。
🔥非类型模板参数
非类型模板参数(Non-type Template Parameters)是C++模板编程中的一个概念,它允许模板参数不是类型,而是特定的值。这些值在编译时必须是常量表达式,因为模板实例化需要在编译时完成。非类型模板参数为模板的灵活性和表达能力提供了额外的维度。
代码案例:
template<typename T, size_t N>
class MyArray {
public:T& operator[](size_t i) {return arr[i];}const T& operator[](size_t i) const {return arr[i];}private:T arr[N]; // 使用非类型模板参数N指定数组大小
};int main()
{// 实例化MyArray,指定类型为int,大小为5 MyArray<int, 5> myArray;return 0;
}
在上述代码中,模板中N就是非类型的形参,作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。
注:
- 浮点数、类对象以及字符串是不允许作为非类型模板参数的(C++20版本后同时支持了浮点型等内置类型)。
- 非类型的模板参数必须在编译期就能确认结果。
🔥模板的特化
概念
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理,比如:实现了一个专门用来进行小于比较的函数模板。
// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{return left < right;
}
int main()
{cout << Less(1, 2) << endl; // 可以比较,结果正确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;
}
Less在大多数情况下都可以正常比较,但当出现某些特殊场景是,就极容易得出错误结果。上述实例中,p1指向的d1显然小于p2指向的d2对象,但是计算机运算时比较的并不是两个日期,而是两个指针所指位置的大小,这就会导致比较发生错误。
这时候,就需要对模板进行特化(也就是特殊化处理),即:在原模版的基础上,针对特殊类型进行特殊化的实现方式。
模板特化被分为函数模板特化与类模板特化。
函数模板特化
函数模板的特化步骤:
- 必须要先有一个基础的函数模板
- 关键字template后面接一对空的尖括号<>
- 函数名后跟一对尖括号,尖括号中指定需要特化的类型
- 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
以日期对象大小比较函数为例:
// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{return left < right;
}// 对Less函数模板进行特化
template<>
bool Less<Date*>(Date* left, Date* right)
{return *left < *right;
}int main()
{cout << Less(1, 2) << endl;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;
}
实话说,仔细想想,其实并不需要大费心思去考虑来一波函数模板的特化,之前学过的函数重载就能完美解决问题。
bool Less(Date* left, Date* right)
{return *left < *right;
}
该种实现简单明了,代码的可读性高,容易书写。
类模板特化
全特化
全特化:将模板参数列表中所有的参数都确定化。
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;
};void TestVector()
{Data<int, int> d1;Data<int, char> d2;// 调用全特化版本
}
偏特化
偏特化:任何针对模版参数进一步进行条件限制设计的特化版本。
比如对以下模板类:
template<class T1, class T2>
class Data
{
public:Data() {cout<<"Data<T1, T2>" <<endl;}
private:T1 _d1;T2 _d2;
};
偏特化有以下两种表现方式:
- 部分特化
将模板参数类表中的一部分参数特化。
// 将第二个参数特化为int
template <class T1>
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<double, int> d1; // 调用特化的int版本Data<int, double> d2; // 调用基础的模板Data<int*, int*> d3; // 调用特化的指针版本Data<int&, int&> d4(1, 2); // 调用特化的指针版本return 0;
}
🔥模板分离编译
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。
为什么不建议声明和定义分离?
那么来填坑,为什么不建议我们将模板声明和定义分离?
假设有如下场景,模板的声明与定义分离开,在头文件中进行声明,源文件中完成定义:
// 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模板不知道需要实例化哪些类型的函数,导致最后无法提供main.cpp
函数中所需的Add调用。
解决方案
- 在模板定义的位置进行显示实例化(不推荐,麻烦)
// a.cpp(源文件)
template<class T>
T Add(const T& left, const T& right)
{return left + right;
}
// 显示实例化
template
double Add(const double& left, const double& right);
template
int Add(const int& left, const int& right);
- 将声明和定义放到同一个文件
xxx.hpp
或xxx.h
里面(推荐)
这种解决方案,才能真正体现了模板对编程带来的便利。
总结
【优点】
- 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
- 增强了代码的灵活性
【缺陷】
- 模板会导致代码膨胀问题,也会导致编译时间变长
- 出现模板编译错误时,错误信息非常凌乱,不易定位错误
🌈结语
本篇博客到这里就要结束了,模板进阶的结束也代表着C++初阶语法告一段落。后面我们会接触更多有意思的内容,包括继承,多态以及C++11新语法。同时也会开始我们的数据结构进阶:平衡二叉树,AVL树,红黑树。更便捷的STL工具:map,set等。感谢大家的支持♥