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

导读:本篇文章讲解 C++深入浅出(七)—— 模板进阶,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com


前言

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

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

1. 非类型模板参数

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

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

那么我希望 st1 的大小为 100,st2 的大小为 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)出现模板编译错误时,错误信息非常凌乱,不易定位错误。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之家整理,本文链接:https://www.bmabk.com/index.php/post/80820.html

(0)
小半的头像小半

相关推荐

极客之家——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!