C++深入浅出(七)—— 模板进阶

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

文章目录


前言

之前我写过一篇关于 C++ 的模板文章C++模板初阶

那么今天这篇文章将在模板初阶的基础上继续深入研究

1. 非类型模板参数

假设我现在自定义了一个静态栈栈的大小设置为 100。

然后我构建了一个 int 的类型的栈 st1和一个 double 类型的栈 st2

那么我希望 st1 的大小为 100st2 的大小为 500能不能实现呢

肯定是不能的

#define N 100

// 静态栈
template<class T>
class Stack
{
private:
	int _a[N];
	int _top;
};

int main()
{
	Stack<int> st1;
	Stack<double> st1;

	return 0;
}

那么有什么办法可以解决这个问题呢

这时候就引出了 非类型模板参数

我们知道模板参数分为类型形参与非类型形参

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

既然这样的话那么我们可以给上面的栈添加非类型模板参数这样就是实现了 st1st2 构造不同的大小。

// 静态栈
template<class T, size_t N>
class Stack
{
private:
	int _a[N];
	int _top;
};

int main()
{
	Stack<int, 100> st1;
	Stack<double, 500> st2;

	return 0;
}

我们还可以通过调试看一下

在这里插入图片描述

注意

  • 浮点数、类对象以及字符串是不允许作为非类型模板参数的。
  • 非类型的模板参数必须在编译期就能确认结果。

2. 模板的特化

🍑 概念

通常情况下使用模板可以实现一些与类型无关的代码但对于一些特殊类型的可能会得到一些错误的结果需要特殊处理。

就拿我们前面实现过的日期类来说比如我要实现了一个专门用来进行小于比较的函数模板。

关于日期类的实现可以参考这一篇文章日期类的实现

#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; // 可以比较结果正确

	return 0;
}

可以看到不管是内置类型还是自己实现的日期类都可以通过 Less 函数模板来比较大小而且结果都是正确的

在这里插入图片描述

那如果我要比较指针类型呢

#include "Date.h"

// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
	return left < right;
}

int main()
{
	Date* p1 = new Date(2022, 12, 23);
	Date* p2 = new Date(2022, 12, 24);
	cout << Less(p1, p2) << endl;

	return 0;
}

我们运行发现结果是正确的呀23 确实小于 24 呀

在这里插入图片描述

如果我们再运行一次可以看到竟然变成了 0 了也就是说 23 小于 24 为 false 的

在这里插入图片描述

也就是说Less 绝对多数情况下都可以正常比较但是在特殊场景下就得到错误的结果。

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

此时就 需要对模板进行特化

在原模板类的基础上针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化与类模板特化。

🍑 函数模板特化

函数模板的特化步骤

  • 必须要先有一个基础的函数模板
  • 关键字 template 后面接一对空的尖括号 <>
  • 函数名后跟一对尖括号尖括号中指定需要特化的类型
  • 函数形参表:必须要和模板函数的基础参数类型完全相同如果不同编译器可能会报一些奇怪的错误。

代码示例

#include "Date.h"

// 函数模板 -- 参数匹配
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()
{
	Date* p1 = new Date(2022, 12, 23);
	Date* p2 = new Date(2022, 12, 24);
	cout << Less(p1, p2) << endl;

	Date* p3 = new Date(2022, 12, 20);
	Date* p4 = new Date(2022, 12, 10);
	cout << Less(p3, p4) << endl;

	return 0;
}

此时就会去调用特化之后的版本而不走模板生成了。

在这里插入图片描述

注意一般情况下如果函数模板遇到不能处理或者处理有误的类型为了实现简单通常都是将该函数直接给
出。

bool Less(Date* left, Date* right)
{
	return *left < *right;
}

该种实现简单明了代码的可读性高容易书写。因为对于一些参数类型复杂的函数模板特化时才会特别给出因此函数模板不建议特化。

🍑 类模板特化

除了函数模板可以进行特化类模板也可以特化主要分为全特化和偏特化。

🍅 全特化

全特化即是将模板参数列表中所有的参数都确定化。

假设有下面这样一个 Data 类我希望构建的 d2 对象里面 T1intT2double有什么办法吗

template<class T1, class T2>
class Data
{
public:
	Data()
	{
		cout << "Data<T1, T2>" << endl;
	}
private:
	T1 _d1;
	T2 _d2;
};

int main()
{
	Data<int, int> d1;
	Data<int, double> d2;

	return 0;
}

我们实例化 d1d2 对象时编译器会自动调用其默认构造函数当我们打印的时候可以看到实际上 d2 对象里面还是 int

在这里插入图片描述

那么这个时候那么我们就可以对 T1T2 分别是 doubleint 时的模板进行特化。

template<class T1, class T2>
class Data
{
public:
	Data()
	{
		cout << "Data<T1, T2>" << endl;
	}
private:
	T1 _d1;
	T2 _d2;
};


// 全特化
template<>
class Data<int, double>
{
public:
	Data()
	{
		cout << "Data<int, double>" << endl;
	}
private:
	int _d1;
	double _d2;
};

int main()
{
	Data<int, int> d1;
	Data<int, double> d2;

	return 0;
}

当我们运行以后可以看到 d2 对象就去调用刚刚写好的特化类模板。

在这里插入图片描述

🍅 偏特化

偏特化也叫半特化任何针对模版参数进一步进行条件限制设计的特化版本。

我们还是拿下面这个 Data 类来举例说明

template<class T1, class T2>
class Data
{
public:
	Data()
	{
		cout << "Data<T1, T2>" << endl;
	}
private:
	T1 _d1;
	T2 _d2;
};

总的来说偏特化有两种表现方式部分特化和参数更进一步的限制。

1部分特化

将模板参数类表中的一部分参数特化。

比如我们对 T1 类型进行特化处理固定其类型为 double

template<class T1, class T2>
class Data
{
public:
	Data()
	{
		cout << "Data<T1, T2>" << endl;
	}
private:
	T1 _d1;
	T2 _d2;
};

// 部分特化 -- 将第一个参数特化为double
template<class T2>
class Data<double, T2>
{
public:
	Data()
	{
		cout << "Data<double, T2>" << endl;
	}
private:
	double _d1;
	T2 _d2;
};

int main()
{
	Data<int, int> _d1;
	Data<double, double> _d2;
	Data<double, char> _d3;

	return 0;
}

可以看到当我们指定 T1double 的时候才会调用这个部分特化的类模板。

在这里插入图片描述

2参数更进一步的限制

偏特化并不仅仅是指特化部分参数而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。

代码示例

// 基础模板
template<class T1, class T2>
class Data
{
public:
	Data()
	{
		cout << "Data<T1, T2>" << endl;
	}
private:
	T1 _d1;
	T2 _d2;
};

// 部分特化 -- 将第一个参数特化为double
template<class T2>
class Data<double, T2>
{
public:
	Data()
	{
		cout << "Data<double, T2>" << endl;
	}
private:
	double _d1;
	T2 _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, int> d1; // 调用基础的版本

	Data<double, double> d2; // 调用部分特化的double版本

	Data<int*, int*> d3; // 调用特化的指针版本

	Data<int&, int&> d4(2, 4); // 调用特化的引用版本

	return 0;
}

运行以后可以看到当我们实例化的对象为 指针类型 或者 引用类型 的时候就会去调用这两个特化模板。

在这里插入图片描述

🍑 类模板特化应用示例

我们还是拿日期类来举例假设我现在要对 3 个实例化对象进行排序

#include "Date.h"
#include<vector>
#include <algorithm>

// Less模板 --- 比较小于
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>());

	// 打印
	for (auto e : v1)
	{
		cout << e;
	}

	return 0;
}

可以看到此时是能直接排序的结果是日期升序。

在这里插入图片描述

那如果我 vector 里面存放的是 Date* 类型的数据还能排序吗

#include "Date.h"
#include<vector>
#include <algorithm>

// Less模板 --- 比较小于
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*> v2;
	v2.push_back(&d1);
	v2.push_back(&d2);
	v2.push_back(&d3);

	// 排序
	sort(v2.begin(), v2.end(), Less<Date*>());

	// 打印
	for (auto e : v2) {
		cout << e << endl;
	}

	return 0;
}

因为 v2 当中存放的地址所以我们打印的时候要解引用打印以后看到日期还不是升序呀那么我们排序的到底是什么呢

在这里插入图片描述

如果我们不解引用直接打印 v2 的每个元素可以看到 v2 中放的地址是升序的。因为此处需要在排序过程中让 sort 比较 v2 中存放地址指向的日期对象但是走了 Less 模板sort 在排序时实际比较的是 v2 中指针的地址因此无法达到预期。

在这里插入图片描述

通过观察上述程序的结果发现对于日期对象可以直接排序并且结果是正确的。但是如果待排序元素是指针结果就不一定正确。

因为sort 最终按照 Less 模板中的方式比较所以只会比较指针而不是比较指针指向空间中内容。

那么此时可以使用 类版本特化 来处理上述问题

#include "Date.h"
#include<vector>
#include <algorithm>

// Less模板 --- 比较小于
template<class T>
struct Less
{
	bool operator()(const T& x, const T& y) const
	{
		return x < y;
	}
};

// 对Less类模板按照指针方式特化
template<>
struct Less<Date*>
{
	bool operator()(Date* x, Date* y) const
	{
		return *x < *y;
	}
};

int main()
{
	Date d1(2022, 7, 7);
	Date d2(2022, 7, 6);
	Date d3(2022, 7, 8);

	vector<Date*> v2;
	v2.push_back(&d1);
	v2.push_back(&d2);
	v2.push_back(&d3);

	// 排序
	sort(v2.begin(), v2.end(), Less<Date*>());

	// 打印
	for (auto e : v2) {
		cout << *e;
	}

	return 0;
}

特化之后再运行就可以得到正确的排序结果了

在这里插入图片描述

3. 模板分离编译

🍑 什么是分离编译

一个程序项目由若干个源文件共同实现而每个源文件单独编译生成目标文件最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。

🍑 模板的分离编译

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

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(2.0, 2.0);

	return 0;
}

可以看到编译是不能通过的

在这里插入图片描述

那么是什么原因导致的呢

我们知道 C/C++ 程序要运行一般要经历以下几个步骤

  • 预处理
  • 编译对程序按照语言特性进行词法、语法、语义分析错误检查无误后生成汇编代码。注意头文件不参与编译 编译器对工程中的多个源文件是分离开单独编译的。
  • 汇编
  • 链接将多个 obj 文件合并成一个并处理没有解决的地址问题。

分析

在这里插入图片描述

🍑 解决办法

解决方法其实也很简单

  • 将声明和定义放到一个文件 "xxx.hpp" 里面或者 xxx.h 其实也是可以的。推荐使用这种
  • 模板定义的位置显式实例化。这种方法不实用不推荐使用

那么我们把声明和定义放到一个 a.hpp 文件里面

a.hpp 声明和定义

template<class T>
T Add(const T& left, const T& right);

template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}

main.cpp 主函数文件

#include"a.hpp"

int main()
{
	Add(1, 2);
	Add(2.0, 2.0);

	return 0;
}

此时编译就通过了然后打印结果也是正确的

在这里插入图片描述

4. 模板总结

优点

1模板复用了代码节省资源更快的迭代开发C++ 的标准模板库STL因此而产生。

2增强了代码的灵活性。

缺点

1模板会导致代码膨胀问题也会导致编译时间变长。

2出现模板编译错误时错误信息非常凌乱不易定位错误。

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: c++