【C++】模板进阶

文章目录

  • 前言
  • [1. 非类型模板参数](#1. 非类型模板参数)
    • [1.1 类型形参](#1.1 类型形参)
    • [1.2 非类型形参](#1.2 非类型形参)
  • [2. 模板的特化](#2. 模板的特化)
    • [2.1 概念](#2.1 概念)
    • [2.2 函数模板特化](#2.2 函数模板特化)
    • [2.3 类模板特化](#2.3 类模板特化)
      • [2.3.1 全特化](#2.3.1 全特化)
      • [2.3.2 偏特化](#2.3.2 偏特化)
      • [2.3.3 类模板特化应用示例](#2.3.3 类模板特化应用示例)

前言

本文深入探讨了C++模板的高级特性,主要包括非类型模板参数和模板特化。


想看模板初阶的道友可以点这里:【C++】模板初阶

1. 非类型模板参数

模板参数分为类型形参与非类型形参两种,我们之前定义的那种都是类型模板参数。

1.1 类型形参

类型形参即出现在模板参数列表中,跟在class或者typename之类的参数类型名称。

比如:

cpp 复制代码
template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}

在该函数模板中,T 就是类型模板参数,是在使用模板时具体指定的类型

1.2 非类型形参

非类型形参,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。

先来看一个场景:假设我们现在要定义一个静态的栈

cpp 复制代码
#define N 100

template<class T>
class Stack
{
public:
	// ...
private:
	T _arr[N];
	size_t _top;
};

我们如果要修改栈的大小修改宏就可以了,但是这么写会有一些问题,比如我们要求定义的两个栈一个大小是10,另一个是100

cpp 复制代码
Stack<int> st1; // 10
Stack<int> st2; // 100

很明显这里我们做不到让两个栈一个大小是10,另一个是100,在同一份代码里面只能保持 N 是一个大小,那这就很难受了,但是非类型模板参数就能很好的解决这个问题

cpp 复制代码
template<class T, size_t n = 10>
class Stack
{
public:
	// ...
private:
	T _arr[n];
	size_t _top;
};

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

	return 0;
}

非类型模板参数也能给缺省值

注意:

  1. 非类型模板参数一般要求只能是整型
  2. 其它类型如浮点数、类对象以及字符串是不允许作为非类型模板参数的。
  3. 非类型的模板参数必须在编译期间就能确认结果。

2. 模板的特化

2.1 概念

特化就是模板的特殊化,通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理,比如下面这种场景

cpp 复制代码
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}

	bool operator<(const Date& d)const
	{
		return (_year < d._year) ||
			(_year == d._year && _month < d._month) ||
			(_year == d._year && _month == d._month && _day < d._day);
	}

	bool operator>(const Date& d)const
	{
		return (_year > d._year) ||
			(_year == d._year && _month > d._month) ||
			(_year == d._year && _month == d._month && _day > d._day);
	}

private:
	int _year;
	int _month;
	int _day;
};

// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
	return left < right;
}
int main()
{
	cout << Less(1, 2) << endl; // 可以比较,结果正确
	Date d1(2025, 12, 6);
	Date d2(2025, 12, 1);
	cout << Less(d1, d2) << endl; // 可以比较,结果正确
	Date* p1 = &d1;
	Date* p2 = &d2;
	cout << Less(p1, p2) << endl; // 可以比较,结果错误
	return 0;
}

运行截图:

一般情况下,Less 都是没问题的,但是当我们传 d1 和 d2 的地址时,less 比较的是 d1 和 d2 的地址而不是空间里面的值,这个时候就出错了。

那这个时候,我们就可以对模板进行特化。特化不能单独存在,即要在原模板类的基础上,针对特殊类型进行特殊化的实现方式。模板特化中分为函数模板特化与类模板特化。

2.2 函数模板特化

函数模板的特化步骤:

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

还是上面的那个例子,如果我们要进行特化

cpp 复制代码
// 函数模板 -- 参数匹配
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()
{
	cout << Less(1, 2) << endl; // 可以比较,结果正确
	Date d1(2025, 12, 6);
	Date d2(2025, 12, 1);
	cout << Less(d1, d2) << endl; // 可以比较,结果正确
	Date* p1 = &d1;
	Date* p2 = &d2;
	cout << Less(p1, p2) << endl; // 可以比较,结果正确
	return 0;
}

此时,比较 p1 和 p2 的大小就对了,运行截图:


虽然函数模板的特化语法讲完了,但一般用的很少,所以也不建议用,因为有的时候会很坑,比如还是之前那个函数模板

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

其实按理来说这段代码我们一般也不会直接这么写,因为考虑到自定义类型,尤其是深拷贝的自定义类型,为了减少拷贝应该要加上引用,而且最好再加上 const,这样 const 对象和普通对象都可以调用:

cpp 复制代码
// 函数模板 -- 参数匹配
template<class T>
bool Less(const T& left, const T& right)
{
	return left < right;
}

我们要对这个函数模板进行特化,如果对语法不是特别清楚的话,就会非常的坑

先来看错误写法:

cpp 复制代码
// 函数模板 -- 参数匹配
template<class T>
bool Less(const T& left, const T& right)
{
	return left < right;
}

template<>
bool Less<Date*>(const Date*& left, const Date*& right)
{
	return *left < *right;
}

看一下它的报错:

报错的原因是函数模板中 const 修饰的是 left 和 right 本身,但我们特化的版本中 const 修饰的确是指向的内容,所以匹配不上,下面来看正确的写法:

cpp 复制代码
// 函数模板 -- 参数匹配
template<class T>
bool Less(const T& left, const T& right)
{
	return left < right;
}

template<>
bool Less<Date*>(Date* const& left, Date* const& right)
{
	return *left < *right;
}

回到正题,一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。就比如上面的函数模板特化我们其实可以直接写成一个普通函数。

cpp 复制代码
bool Less(Date* left, Date* right)
{
return *left < *right;
}

这种写法相较于函数模板的特化来说就要更简单明了,代码的可读性也更高,更容易书写。个人感觉函数模板的特化也不太好用,一般函数模板的特化也用的很少,因此函数模板建议还是不要用特化,简单了解一下语法即可。

2.3 类模板特化

2.3.1 全特化

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

cpp 复制代码
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}

	bool operator<(const Date& d)const
	{
		return (_year < d._year) ||
			(_year == d._year && _month < d._month) ||
			(_year == d._year && _month == d._month && _day < d._day);
	}

	bool operator>(const Date& d)const
	{
		return (_year > d._year) ||
			(_year == d._year && _month > d._month) ||
			(_year == d._year && _month == d._month && _day > d._day);
	}

private:
	int _year;
	int _month;
	int _day;
};

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;
};

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

	return 0;
}

2.3.2 偏特化

偏特化是任何针对模版参数进一步进行条件限制设计的特化版本。比如还是以这个模板为例:

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

首先偏特化有部分特化参数更进一步的限制两种表现方式:

  1. 部分特化

部分特化顾名思义就是将模板参数类表中的一部分参数特化。

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

template<class T1>
class Data<T1, char>
{
public:
	Data() { cout << "Data<T1, char>" << endl; }
//private:
// 一样的道理,这里变量写不写都可以
//	T1 _d1;
//	char _d2;
};
  1. 参数更进一步的限制

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

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

//两个参数偏特化为指针类型
template <class T1, class T2>
class Data <T1*, T2*>
{
public:
	Data() 
	{ 
		cout << "Data<T1*, T2*>" << endl;
	}
private:
	T1 _d1;
	T2 _d2;
	// 注意这里即可以用 T1,T2 来定义值,也可以用 T1*,T2* 来定义值
	// 下面两个特化和这里是一样的道理
};

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

//还可以指针和引用混在一起
template <class T1, class T2>
class Data <T1*, T2&>
{
public:
	Data()
	{
		cout << "Data<T1*, T2&>" << endl;
	}
private:
	T1 _d1;
	T2 _d2;
};

int main()
{
	Data<float*, char*> d1;
	Data<float&, char&> d2;
	Data<float*, char&> d3;
	return 0;
}

运行截图:

可以直接排序,结果错误日期还不是升序,而v2中放的地址是升序

// 此处需要在排序过程中,让sort比较v2中存放地址指向的日期对象

// 但是走Less模板,sort在排序时实际比较的是v2中指针的地址,因此无法达到预期

2.3.3 类模板特化应用示例

以小于的类模板来举例:

cpp 复制代码
#include <iostream>
#include<vector>
#include<algorithm>
using namespace std;

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}

	bool operator<(const Date& d)const
	{
		return (_year < d._year) ||
			(_year == d._year && _month < d._month) ||
			(_year == d._year && _month == d._month && _day < d._day);
	}

	bool operator>(const Date& d)const
	{
		return (_year > d._year) ||
			(_year == d._year && _month > d._month) ||
			(_year == d._year && _month == d._month && _day > d._day);
	}

private:
	int _year;
	int _month;
	int _day;

};

template<class T>
struct Less
{
	bool operator()(const T& x, const T& y) const
	{
		return x < y;
	}
};

int main()
{
	Date d1(2025, 12, 6);
	Date d2(2025, 12, 5);
	Date d3(2025, 12, 7);
	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);
	// 可以排序,但结果错误
	sort(v2.begin(), v2.end(), Less<Date*>());
	return 0;
}

在这段代码中,Less 类模板是一个仿函数,它可以调整我们 sort 函数排的是升序还是降序,这里比较小于是降序的逻辑,简单了解一下,后面会细讲

cpp 复制代码
template<class T>
struct Less
{
	bool operator()(const T& x, const T& y) const
	{
		return x < y;
	}
};

回到正题,这段代码中,对于自定义类型,我们直接可以比较大小,但是如果传的是指针,就不行了,这里我们就可以对 Less 类模板按照指针方式特化

cpp 复制代码
template<>
struct Less<Date*>
{
bool operator()(Date* x, Date* y) const
{
return *x < *y;
}
};

这样一来,我们传指针类型也能比较出正确结果


完!

相关推荐
毕设源码-钟学长1 小时前
【开题答辩全过程】以 基于Python爬虫的二手房信息爬取及分析为例,包含答辩的问题和答案
开发语言·爬虫·python
layman05281 小时前
在python中受限于GIL,进程中只允许一个线程处于允许状态,多线程无法充分利用CPU多核
开发语言·python
我不会插花弄玉1 小时前
类与对象-下【由浅入深-C++】
c++
捧 花1 小时前
Go Web 开发流程
开发语言·后端·golang·restful·web·分层设计
南猿北者1 小时前
go语言基础语法
开发语言·后端·golang
京井1 小时前
从中序与后序遍历序列构造二叉树解题思路
c语言·算法
Tandy12356_1 小时前
手写TCP/IP协议栈——ARP超时重新请求
c语言·c++·网络协议·计算机网络
CC.GG1 小时前
【Qt】Qt初识
开发语言·qt
水天需0102 小时前
VS Code C++ 环境配置及 HelloWorld 程序
c++