首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >【C++】泛型编程:吃透模板

【C++】泛型编程:吃透模板

作者头像
利刃大大
发布2025-02-05 08:15:21
发布2025-02-05 08:15:21
37900
代码可运行
举报
文章被收录于专栏:csdn文章搬运csdn文章搬运
运行总次数:0
代码可运行

Ⅰ. 泛型编程

一、引入

​ 在 C 语言中,如果我们想写多类型的,并且是同一个函数出来的函数,我们只能要几个写几个出来,这样子会显得比较冗余,也加大了程序员的代码量,于是 c++ 中就引入了 函数重载泛型编程 的概念,大大的简化了我们的工作!

​ 仅靠函数重载是完不成泛型编程的需求的,比如说下面的代码:

代码语言:javascript
代码运行次数:0
运行
复制
void Swap(int& x, int& y) 
{
	int tmp = x;
	x = y;
	y = tmp;
}

void Swap(double& x, double& y) 
{
	double tmp = x;
	x = y;
	y = tmp;
}

void Swap(char& x, char& y) 
{
	char tmp = x;
	x = y;
	y = tmp;
}
 
int main()
{
	int a = 0, b = 1;
	double c = 1.1, d = 2.2;
	char e = 'a', f = 'b';
 
	Swap(a, b);
	Swap(c, d);
	Swap(e, f);
 
	return 0;
}

​ 好像靠函数重载来调用不同类型的 Swap,只是表面上看起来 “通用” 了 ,实际上问题还是没有解决,有新的类型,还是要添加对应的函数。

​ 用函数重载解决的缺陷:

  1. 重载的函数仅仅是类型不同,代码的复用率很低,只要有新类型出现就需要增加对应的函数。❌
  2. 代码的可维护性比较低,一个出错可能导致所有重载均出错。❌

二、什么是泛型编程

​ 泛型编程:编写与类型无关的通用代码,是 代码复用的一种手段。而模板是泛型编程的基础。

​ 其中,模板分为两类,一类是 函数模板,一类是 类模板

Ⅱ. 函数模板

一、函数模板概念

​ 函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。

二、函数模板格式

代码语言:javascript
代码运行次数:0
运行
复制
template<class T1, class T2, class T3, ........., class Tn>
返回值类型  函数名(参数列表) {函数体}

template定义模板 的关键字,后面跟着尖括号 <>

class 是用来 定义模板参数 的关键字 (也可以用 typename

T1, T2, …, Tn 表示的是函数名,可以理解为模板的名字,名字你可以自己取。

代码语言:javascript
代码运行次数:0
运行
复制
template<typename T>      // 模板参数列表 ———— 参数类型
void Swap(T& x, T& y)     // 函数参数列表 ———— 参数对象
{    
	T tmp = x;
	x = y;
	y = tmp;
}

📌注意事项:

函数模板不是一个函数,因为它不是具体要调用的某一个函数,只是一副蓝图。就像 “好学生”,主体是学生,“好” 是形容 “学生” 的;这里也一样,“函数模板” 是模板,所以函数模板表达的意思是 “函数的模板” 。

​ 所以,我们一般不叫它模板函数,应当叫作函数模板。而 模板函数是一种用模板实例化出来的函数。

三、函数模板的原理

​ 函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实 模板就是将本来应该我们做的重复的事情交给了编译器去做

​ 在编译器 编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用 double 类型使用函数模板时,编译器通过对实参类型的推演,将 T 确定为 double 类型,然后产生一份专门处理 double 类型的代码,对于字符类型也是如此。

通俗易懂的说,就是这里三个不同参数类型函数,不是同一个函数,我们只负责传类型参数,而生成这些函数的工作由编译器替我们做!

四、函数模板的实例化

​ 用不同类型的参数使用模板参数时,称为函数模板的实例化。其中模板参数实例化分为:隐式实例化显式实例化 ,下面我们来分别讲解一下这两种实例化。

① 隐式实例化

📚 定义:让编译器根据实参,推演模板函数的实际类型

​ 以下面 add 函数代码为例子:

代码语言:javascript
代码运行次数:0
运行
复制
template<class T>
T Add(const T& x, const T& y) 
{
	return x + y;
}
 
int main()
{
	int a1 = 10, a2 = 20;
	double d1 = 10.1, d2 = 20.2;
	cout << Add(a1, a2) << endl;
	cout << Add(d1, d2) << endl;
 
	return 0;
}

// 运行结果:
30
30.3

现在思考一个问题,如果出现 int + double 不同类型相加这种情况呢❓ 实例化能成功吗❓

代码语言:javascript
代码运行次数:0
运行
复制
Add(a1, d2);

​ 最后程序报错了,也就是编译器无法根据一个 T 来推出两个类型想要用哪个。

💡 解决方法:

1. 传参之前先进行强制类型转换

代码语言:javascript
代码运行次数:0
运行
复制
template<class T>
T Add(const T& x, const T& y) 
{
	return x + y;
}
 
int main()
{
	int a1 = 10, a2 = 20;
	double d1 = 10.1, d2 = 20.2;
	cout << Add(a1, a2) << endl;
	cout << Add(d1, d2) << endl;
 
	cout << Add((double)a1, d2) << endl;  // 将a1强转为double,或者将d2强转为int都行
 
	return 0;
}

2. 写两个模板参数,其中返回的参数类型起决定性作用

代码语言:javascript
代码运行次数:0
运行
复制
template<class T1, class T2>
T1 Add(const T1& x, const T2& y)   // 那么T1就是int,T2就是double
{ 
	return x + y;      // 范围小的会向范围大的提升,也就是int会转为double
} // 最后表达式会是一个double,但是最后返回值又是T1,是int,又会转
 
int main(void)
{
	int a1 = 10, a2 = 20;
	double d1 = 10.1, d2 = 20.2;
 
	cout << Add(a1, d2) << endl;   // int,double  👆
 
	return 0;
}

3. 使用 “显式实例化” 来解决

代码语言:javascript
代码运行次数:0
运行
复制
Add<int>(a1, d2);     // 指定实例化成int
Add<double>(a1, d2)   // 指定实例化成double
② 显式实例化

​ 📚 定义:在函数名后的 <> 里指定模板参数的实际类型。

​ 简单来说,显式实例化就是在中间加一个尖括号 <> 去指定你要实例化的类型。(在函数名和参数列表中间加尖括号)

代码语言:javascript
代码运行次数:0
运行
复制
函数名 <类型> (参数列表);

​ 所以对于上面的问题,我们可以用该方法解决:

代码语言:javascript
代码运行次数:0
运行
复制
template<class T>
T Add(const T& x, const T& y) 
{
	return x + y;
}
 
int main()
{
	int a1 = 10, a2 = 20;
	double d1 = 10.1, d2 = 20.2;
	cout << Add(a1, a2) << endl;
	cout << Add(d1, d2) << endl;
 
	cout << Add<int>(a1, d2) << endl;     // 指定T用int类型
	cout << Add<double>(a1, d2) << endl;  // 指定T用double类型
 
	return 0;
}

// 运行结果:
30
30.3
30
30.2

🔑 解读:

​ 像第一个 Add<int>(a1, a2)a2double,它就要转换成 int 。第二个 Add<double>(a1, a2)a1int,它就要转换成 double。像这种地方就是类型不匹配的情况,编译器会尝试进行隐式类型转换。像 doubleint 这种相近的类型,是完全可以通过隐式类型转换的。

🔺总结:

  • 函数模板你可以让它自己去推,但是推的时候不能自相矛盾。
  • 你也可以选择去显式实例化,去指定具体的类型。

五、模板参数的匹配原则

一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数

代码语言:javascript
代码运行次数:0
运行
复制
// 专门处理int的加法函数
int Add(int x, int y) {
	return x + y;
}
 
// 通用加法函数
template<class T>
T Add(const T& x, const T& y) {
	return x + y;
}
 
int main()
{
	Add(1, 2); // 与非模板函数匹配,编译器不需要特化
 	Add<int>(1, 2); // 调用编译器特化的Add版本
 
	return 0;
}

对于非模板函数和同名函数模板,如果其他条件都相同,在调用时会优先调用非模板函数,而不会从该模板生成一个实例。如果模板可以产生一个具有更好匹配的函数,那么将选择模板。

代码语言:javascript
代码运行次数:0
运行
复制
// 专门处理int的加法函数
int Add(int left, int right) 
{
	return left + right; 
}
// 通用加法函数
template<class T1, class T2>
T1 Add(T1 left, T2 right) 
{
	return left + right; 
}
void Test()
{
	Add(1, 2);   // 与非函数模板类型完全匹配,不需要函数模板实例化
	Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数
}

模板函数不允许自动类型转换,但普通函数可以进行自动类型转换

🔺总结:

  • 函数调用时候,如果有现成的函数,绝对不去实例化模板,因为这样子也会导致重复。
  • 调用时候,若几个参数都有符合,则 优先匹配类型最合适的

Ⅲ. 类模板

一、类模板的定义格式

代码语言:javascript
代码运行次数:0
运行
复制
template <class T1, class T2, ..., class Tn>
class 类模板名 
{
    // 类内成员定义
}

​ 有了类模板,我们就有得天独厚的优势:不用再去 typedef 类型

​ 我们之前 C 语言中对于一些数据结构比如 stack,我们在设置 int 类型的时候,只能去 typedefint,然后如果需要 double 则重新去 typedefdouble,非常麻烦,而且这种方式做不到同时申请一个 int 类型和 double 类型的 stack

​ 现在有了类模板,我们只需要在定义声明时候设置它的格式即可!

代码语言:javascript
代码运行次数:0
运行
复制
// 注意:Stack不是具体的类,是编译器根据被实例化的类型生成具体类的模具
template<class T>
class Stack 
{
	// 类内成员定义
};
 
int main()
{
	Stack<int> st1;    // 存储int
	Stack<double> st2; // 存储double
 
	return 0;
}

🍡 注意事项:这里的 Stack 不是具体的类,是编译器根据被实例化的类型生成 具体类的模具

二、类模板的实例化

​ 类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟 <>,然后将实例化的类型放在 <> 中即可,类模板名字不是真正的类,而实例化的结果才是真正的类

代码语言:javascript
代码运行次数:0
运行
复制
类名<类型> 变量名;

// 例如:
Stack<int> st1;
Stack<double> st2;

🍡 注意事项:这里的 Stack类名,而 Stack<int>类型

三、类外定义类模板参数

​ 如果我们在类外定义函数时候这样子定义的话是会报错的:

代码语言:javascript
代码运行次数:0
运行
复制
template<class T>
class Stack {
public:
	Stack(T capacity = 4) 
		: _top(0) 
		, _capacity(capacity) 
    {
		_arr = new T[capacity];
	}
    
	~Stack(); // 这里我们让析构函数放在类外定义
private:
	T* _arr;
	int _top;
	int _capacity;
};
 
/* 类外 */
Stack::~Stack() {    ❌  // 即使是指定类域也不行 
    ...
}

🔑 解答:

Stack 是类名,Stack<int> 才是类型。这里要拿 Stack<T> 去指定类域才对。

② 类模板中的函数在类外定义,没加 “模板参数列表” ,编译器不认识这个 T类模板中函数放在类外进行定义时,需要加模板参数列表。

💬 代码演示: 我们现在来看一下如何添加模板参数列表!

代码语言:javascript
代码运行次数:0
运行
复制
template<class T>
class Stack {
public:
	Stack(T capacity = 4) 
		: _top(0) 
		, _capacity(capacity) {
		_arr = new T[capacity];
	}
 
	// 这里我们让析构函数放在类外定义
	~Stack();
private:
	T* _arr;
	int _top;
	int _capacity;
};
 
// 类模板中函数放在类外进行定义时,需要加模板参数列表
// Stack是类名,而Stack<T> 才是类型
template <class T>
Stack<T>::~Stack() 
{   
	delete[] _arr;
	_arr = nullptr;
	_capacity = _top = 0;
}

知识点小结

  1. 模板可以具有 非类型参数,用于指定大小,可以根据指定的大小创建动态结构(这个下面会讲到)。
  2. 模板运行时不检查数据类型,也不保证数据安全,相当于类型的宏替换
  3. 类模板 是一个类家族,而 模板类 是通过 类模板实例化的具体类
  4. 类模板中的成员函数全是模板函数,在 类外定义时都必须通过完整的模板语法进行定义
  5. 在类模板中,如果成员函数没被调用过的话,编译器是不会生成其代码,以此来减少代码膨胀!
  6. 可以同时用 classtypename 来定义模板,如 template<class T1, typename T2>

Ⅳ. 非类型模板参数(Nontype Template Parameters

一、问题的引入

💭 举例: 假设我们要定义一个静态的数组:

代码语言:javascript
代码运行次数:0
运行
复制
#define X 1000

template<class T>
class Srray
{
	T _arr[X];
};

int main()
{
	Srray<int> a1;	
	Srray<int> a2;  
    
    return 0;
}

​ 这个时候我们发现,这个静态数组的大小是固定死的,我们做不到让 define 运行时候改变数值呀,如果我想让 a11000 个空间,让 a2100 个,那对于 a2 来说,是不是就浪费了 900 个空间了,那这个时候应该怎么做呢?

​ 这个时候就引入了这个非类型模板参数!

二、什么是非类型模板参数?

​ 注意看,我们普通定义的 T 是类型,而 N 这里并不是类型,而是一个常量!类型模板参数定义的是虚拟类型,注重的是你要传什么,而 非类型模板参数定义的是常量

代码语言:javascript
代码运行次数:0
运行
复制
               "非类型模板参数"
                     👇
template<class T, size_t N> class array;
             👆    
        "类型模板参数"

模板参数分为类型形参与非类型形参:

  • 类型形参:出现在模板参数列表中,跟在 class 或者 typename 之类的参数类型名称
  • 非类型形参:就是用一个常量作为类 (函数) 模板的一个参数,在类 (函数) 模板中可将该参数当成常量来使用

​ 既然有了这个非类型模板参数,我们尝试着来解决问题!

代码语言:javascript
代码运行次数:0
运行
复制
//#define X 1000

template<class T, size_t X>
class Srray
{
	T _arr[X];
};

int main()
{
	Srray<int, 100> a1;	  // 开辟了100个空间大的静态数组
	Srray<int, 1000> a2;  // 开辟了1000个空间大的静态数组

	return 0;
}

​ 这里我们在模板这定义一个常量 X,让它去做数组的大小。

​ 于是我们就可以在实例化 Srray 的时候指定其实例化对象的大小了,分别传 1001000

📌 注意事项:

① 非类型模板参数是 整形常量,是不能修改的。

代码语言:javascript
代码运行次数:0
运行
复制
template<class T, size_t X>
class Srray
{
public:
	void Change()
	{
		X = 10;
	}
private:
	T _arr[X];
};

int main()
{
	Srray<int, 100> a1;	  // 开辟了100个空间大的静态数组
	Srray<int, 1000> a2;  // 开辟了1000个空间大的静态数组

	a1.Change();// 编译器会报错,因为X是常量,不能修改

	return 0;
}

② 有些类型是不能作为非类型模板参数的,比如浮点数、类对象以及字符串,也就是说,只能用 size_tintcharchar 也算整型)

代码语言:javascript
代码运行次数:0
运行
复制
template<string S> ❌
class A
{
	// 类内成员定义
};

template<double D> ❌
class B
{
	// 类内成员定义
};

③ 非类型的模板参数必须是在编译期就能确认结果的

三、STL 中的 Array(不重要)

🔍 官方文档:array - C++ Reference

​ 现在学了非类型模板参数了,我们现在再来回头看 array

arrayC++11 新增的,它有什么独特的地方吗?很可惜,基本没有,并且 vector 可以完全碾压 array

代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>
#include <array>
#include <vector>
using namespace std;
 
int main()
{
    vector<int> v1(100, 0);
    array<int, 100> a1;
    
    cout << "size of v1: " << sizeof(v1) << endl;
    cout << "size of a1: " << sizeof(a1) << endl;
 
    return 0;
}

// 🚩 运行结果:
size of v1: 32
size of a1: 400

vector 是开在空间大的堆上的,而 array 是开在寸土寸金的栈上的,堆可比栈的空间大太多太多了。

​ 最尴尬的是 array 能做的操作几乎 vector 都能做,因为 vector 的存在 array 显得有些一无是处。

​ 比起原生数组,array 的最大优势也只是有一个越界的检查,读和写都可以检查到是否越界。要对比的话也只能欺负一下原生数组,然而面对强大的 vectorarray 完全没有招架之力。

🔺 总结: array 相较于原生数组,有越界检查之优势,实际中建议直接用 vector

Ⅴ. 模板的特化

一、引入:给特殊类型准备特殊模板

​ 有时候,编译默认的函数模板和类模板(在一些特殊场景下)不能正确处理需要逻辑,这时候就需要针对这些指定类型进行特殊化处理,所以就要做模板的特化。

💭 举例①: 字符串类型的比较

代码语言:javascript
代码运行次数:0
运行
复制
template<class T>
bool IsSame(const T& a, const T& b)
{
	return a == b;
}

int main()
{
	// 对于一般的类型对比,没有问题
	cout << IsSame(1, 2) << endl;
	cout << IsSame(1.1, 2.2) << endl;

    // 这里好像就出问题!
	char s1[] = "liren";
	char s2[] = "liren";
	cout << IsSame(s1, s2) << endl;

	const char* s3 = "lirendada";
	const char* s4 = "lirendada";
	cout << IsSame(s3, s4) << endl;

	return 0;
}

​ 原因如下图所示:

​ 但也不对啊,字符串我们比较的都是字符大小,而不是比较地址,所以出现了这种情况,模板也没办法帮我们解决,所以我们就得来自己动手特化!

💭 举例②: 自定义类型的比较

代码语言:javascript
代码运行次数:0
运行
复制
#include "Date.h"    /* 引入自己实现的日期类 */
 
/* 判断左数是否比小于右数 */
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 = new Date(2022, 7, 16);
    Date* p2 = new Date(2022, 7, 15);
    cout << Less(p1, p2) << endl;      // 可以比较,结果正确
 
    return 0;
}

❓运行结果:(我们运行几次发现,其结果不稳定,对于 Date* 一会是真一会是假)

​ 上述示例中,p1 指向的 d1 显然小于 p2 指向的 d2 对象,但是 Less 内部并没有比较 p1p2 指向的对象内容,而比较的是 p1p2 指针的地址,这就无法达到预期而错误。

​ 此时,就 需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为 函数模板特化与类模板特化

二、函数模板的特化

函数模板的特化有两种方法:

方法一
  1. 首先,必须要先有一个基础的函数模板。
  2. 其次,关键字 template 后面接上一对空的尖括号 <>
  3. 然后,函数名后跟一对尖括号,尖括号中指定需要特化的内容。
  4. 最后,函数形参表必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
方法二

​ 直接写出需要的函数以及类型,也就是直接写出重载的函数(比较简单一点)。

例子①: 对于字符串的特化

代码语言:javascript
代码运行次数:0
运行
复制
#include<cstring>

template<class T>
bool IsSame(const T& a, const T& b)
{
	return a == b;
}

// 方法一:标准的特化写法
template<>
bool IsSame<const char*>(const char* const& a, const char* const& b)
{
	return strcmp(a, b) == 0;
}

// 方法二:字符数组类型的特化
bool IsSame(char* a, char* b)
{
	return strcmp(a, b) == 0;
}

// 方法二:字符串常量类型的特化
bool IsSame(const char* a, const char* b)
{
	return strcmp(a, b) == 0;
}

int main()
{
	// 对于一般的类型对比,没有问题
	cout << IsSame(1, 2) << endl;
	cout << IsSame(1.1, 2.2) << endl;

	char s1[] = "liren";
	char s2[] = "liren";
	cout << IsSame(s1, s2) << endl;

	const char* s3 = "lirendada";
	const char* s4 = "lirendada";
	cout << IsSame(s3, s4) << endl;

	return 0;
}

// 🚩 运行结果:
0
0
1
1

问题:为什么标准特化写法那里的参数类型是 const char* const& a,而不是 const char*& a ?

💡 解答

​ 因为原来的模板参数的里面是 const T& a,而这里的 const 是用来修饰 a 的,我们特化后仅仅把 T 特化为 const char*,但是原来的类型是 const & 的,根据上面的特化要求第四点,函数形参表必须要和模板函数的基础参数类型完全相同,所以特化后的参数类型必须为将 const & 带上,最后就变成 const char* const& a

​ 也注意不要太抬杠,毕竟语法是被人设定出来,这很大程度取决于设计人当时的想法!

例子②: 对于日期类等自定义类型

代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>
#include "Date.h"
using namespace std;
 
// 必不可少的原版
template<class T>
bool Less(T left, T right) 
{
    return left < right;
}
 
// 针对某些类型要特殊化处理 ———— 使用模板的特化解决
template<>
bool Less<Date*>(Date* left, Date* right) 
{
    return *left < *right;
}

// 直接匹配的普通函数
bool Less(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 = new Date(2022, 7, 16); //动态开辟,在堆区
    Date* p2 = new Date(2022, 7, 15);
    cout << Less(p1, p2) << endl;     
 
    return 0;
}

// 🚩 运行结果:
1
1
0

💡 解读: 对于普通类型,它还是会调正常的模板。对于 Date* 编译器就会发现这里有个专门为 Date* 而准备的特化版本,编译器会优先选择该特化版本。

❓ 思考:现在我们加一个普通函数,Date* 会走哪个版本?

代码语言:javascript
代码运行次数:0
运行
复制
// 原模板
template<class T>
bool Less(T left, T right) 
{
    return left < right;
}
 
// 对模板特化的
template<>
bool Less<Date*>(Date* left, Date* right) 
{
    return *left < *right;
}
 
// 直接匹配的普通函数
bool Less(Date* left, Date* right) 
{
    return *left < *right;
}

🔑 答案: 函数重载,会走直接匹配的普通函数版本,因为有现成的,就不用实例化了。

🔺 结论

​ 函数模板不一定非要特化,因为在参数里面就可以处理,写一个匹配参数的普通函数也更容易理解。该种实现简单明了,代码的可读性高,容易书写,因为对于一些参数类型复杂的函数模板,特化时特别给出,因此函数模板不建议特化,直接给出匹配参数的普通函数即可

三、类模板的特化

​ 刚才函数模板不一定非要特化,因为可以写一个具体实现的函数。但是 类模板我们没法实现一个具体的实际类型,就必须要特化

​ 假设针对模板参数 T1T2 分别是 intint,想对这种特定类型做出特殊化处理,该怎么办,如下所示:

代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>
#include "Date.h"
using namespace std;
 
template<class T1, class T2>
class Data 
{
public:
	fun() {
		cout << "Data<T1, T2>" << endl;
	}
private:
	T1 _d1;
	T2 _d2;
};
 
int main()
{
	Data<int, int> d1; // 想要对其进行特殊化处理
	Data<int, double> d2; 
	return 0;
}

​ 此时特化的方法很简单:增加一个对 intint 的特化处理:

代码语言:javascript
代码运行次数:0
运行
复制
template<>
class Data<int, int>
{
public:
    fun() {
        cout << "Data<int, int>" << endl;
    }
}

​ 有了这个特化后,我们的 d1 就会去我们特化的版本完成定义,而 d2 仍然是去我们的原模板完成定义。

🚩 运行结果:

​ 类模板的特化分为两种:全特化偏特化(也称为半特化)

① 全特化

​ 全特化:将模板参数列表中所有的参数都确定化。比如我们上面写的 Data 的特化就是全特化版本的:

代码语言:javascript
代码运行次数:0
运行
复制
template<>
class Data<int, int>  // 参数列表中的所有参数都是确定的
{
public:
    fun() {
        cout << "Data<int, int>" << endl;
    }
}
② 偏特化(半特化)

​ 半特化: 任何针对模版参数进一步进行条件限制设计的特化版本,将部分参数类表中的一部分参数特化。(半特化并不是特化一半,就像半缺省并不是缺省一半一样)

​ 比如对于以下的模板类:

代码语言:javascript
代码运行次数:0
运行
复制
template<class T1, class T2>
class Data
{
public:
	Data() { cout<<"Data<T1, T2>" <<endl; }
private:
	T1 _d1;
	T2 _d2;
};

偏特化有以下两种表现方式:

  1. 部分特化:将模板参数类表中的一部分参数特化。
代码语言:javascript
代码运行次数:0
运行
复制
// 将第二个参数特化为int
template <class T1>
class Data<T1, int> 
{
public:
	Data() {
        cout<< "Data<T1, int>" <<endl;
    }
};
  1. 参数更进一步的限制:偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。
代码语言:javascript
代码运行次数:0
运行
复制
// 两个参数偏特化为指针类型
template <typename T1, typename T2>
class Data <T1*, T2*>
{ 
public:
	Data() {
        cout<<"Data<T1*, T2*>" <<endl;
    }
};

// 两个参数偏特化为引用类型
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:
     Data() {
     	cout<<"Data<T1&, T2&>" <<endl;
     }
};

void test2 () 
{
     Data<double , int> d1; 	// 调用特化的int版本
     Data<int , double> d2; 	// 调用基础的模板 
     Data<int *, int*> d3;  	// 调用特化的指针版本
     Data<int&, int&> d4(1, 2); // 调用特化的指针版本
}

// 🚩 运行结果:
Data<T1, int>
Data<T1, T2>
Data<T1*, T2*>
Data<T1&, T2&>
③ 类模板特化应用实例

​ 有如下专门用来按照小于比较的类模板 Less:

代码语言:javascript
代码运行次数:0
运行
复制
#include<vector>
#include <algorithm>
template<class T>
struct Less
{
    bool operator()(const T& x, const T& y) const
    {
    	return x < y;
    }
};

int main()
{
	Date d1(2022, 7, 7);
    Date d2(2022, 7, 6);
    Date d3(2022, 7, 8);
    vector<Date> v1;
    v1.push_back(d1);
    v1.push_back(d2);
    v1.push_back(d3);
    
    // 可以直接排序,结果是日期升序
    sort(v1.begin(), v1.end(), Less<Date>());
    
    vector<Date*> v2;
    v2.push_back(&d1);
    v2.push_back(&d2);
    v2.push_back(&d3);

    // 可以直接排序,结果错误,日期还不是升序,而v2中放的地址是升序
    // 此处需要在排序过程中,让sort比较v2中存放地址指向的日期对象
    // 但是走Less模板,sort在排序时实际比较的是v2中指针的地址,因此无法达到预期
    sort(v2.begin(), v2.end(), Less<Date*>());
 	return 0; 
}

​ 通过观察上述程序的结果发现,对于日期对象可以直接排序,并且结果是正确的。但是如果待排序元素是指针,结果就不一定正确。因为:sort 最终按照 Less 模板中方式比较,所以只会比较指针,而不是比较指针指向空间中内容,此时可以使用类版本特化来处理上述问题:

代码语言:javascript
代码运行次数:0
运行
复制
// 对Less类模板按照指针方式特化
template<>
struct Less<Date*>
{
     bool operator()(Date* x, Date* y) const
     {
     	return (*x) < (*y);
     }
};

​ 特化之后,在运行上述代码,就可以得到正确的结果!

Ⅵ. 模板的优缺点

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

Ⅶ. 模板分离编译

​ 假如有以下场景,模板的声明与定义分离开,在头文件中进行声明,源文件中完成定义:

代码语言:javascript
代码运行次数:0
运行
复制
// Add.h文件
template<class T> 
T Add(const T& left, const T& right);

// Add.cpp文件
#include "Add.h"
template<class T> 
T Add(const T& left, const T& right) 
{
	return left + right; 
}


// main.cpp文件
#include "Add.h"
int main()
{
	Add(1, 2);
	Add(1.0, 2.0);
 	return 0; 
}

​ 简单来说,就是模板分离编译的话,定义模板的地方只有定义没有实例化,而实例化的地方只有声明没有定义

💡 解决方法:

  1. 在头文件中实现模板,也就是将声明和定义放到一个文件 xxx.hpp 里面或者 xxx.h 其实也是可以的。推荐使用这种。将模板的实现和声明都放在头文件中,这样在使用模板的时候,编译器能够立即看到模板的实现细节,并生成对应的模板实例化代码。这种方法适用于模板实现代码比较简单的情况,可以避免模板分离编译带来的问题。
  2. 使用显式实例化,这种方法不实用,不推荐使用。在单独的编译单元中,通过显式实例化的方式生成模板的实例化代码,并将其链接到主程序中。这种方法需要在使用模板的文件中显式实例化模板,以告诉编译器需要实例化哪些模板。
    • 这种方法适用于模板实现代码比较复杂的情况,可以避免模板实现暴露在头文件中带来的问题。需要注意的是,显式实例化需要在所有使用模板的文件中都进行,并且需要注意实例化的类型必须与声明时的类型一致,否则会导致链接错误。

【分离编译扩展阅读】 http://blog.csdn.net/pongba/article/details/19130

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-02-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Ⅰ. 泛型编程
    • 一、引入
    • 二、什么是泛型编程
  • Ⅱ. 函数模板
    • 一、函数模板概念
    • 二、函数模板格式
    • 三、函数模板的原理
    • 四、函数模板的实例化
      • ① 隐式实例化
      • ② 显式实例化
    • 五、模板参数的匹配原则
  • Ⅲ. 类模板
    • 一、类模板的定义格式
    • 二、类模板的实例化
    • 三、类外定义类模板参数
  • 知识点小结
  • Ⅳ. 非类型模板参数(Nontype Template Parameters)
    • 一、问题的引入
    • 二、什么是非类型模板参数?
    • 三、STL 中的 Array(不重要)
  • Ⅴ. 模板的特化
    • 一、引入:给特殊类型准备特殊模板
    • 二、函数模板的特化
      • 方法一
      • 方法二
    • 三、类模板的特化
      • ① 全特化
      • ② 偏特化(半特化)
      • ③ 类模板特化应用实例
  • Ⅵ. 模板的优缺点
  • Ⅶ. 模板分离编译
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档