首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >C++模板进阶及特化实战指南

C++模板进阶及特化实战指南

作者头像
用户11831438
发布2025-12-30 14:10:38
发布2025-12-30 14:10:38
1110
举报

一、非类型模板参数

模板参数分为:

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

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

在前面的学习中,我们已经学习类型模板参数,接下来我们一起来看一下非类型模板参数。

在我们现阶段的学习中,非类型模板参数就是指传的是常量(现阶段只能传整型int)

假设现在我想搞一个静态的栈,按照以前的逻辑,我们是不是可以搞个宏——

代码语言:javascript
复制
#define N=10
	template<class T>
	class Stack
	{
	private:
		T _a[N];
		int _top;
	};
代码语言:javascript
复制
void test1()
{
	stack<int> st;//存储10个数据
	stack<int> st1;//存储1000个数据
}

那如果我想存储1000个数据,那是不是就要N=1000,那st中就会有空间浪费

这时候,非类型模板参数就闪亮登场~~~

代码语言:javascript
复制
namespace carrot
{
	template<class T ,size_t N=10>非类型模板参数可以给缺省值
	class Stack
	{
	public:
		Stack()
			:_a(new T[n])
		{}
	private:
		T _a[N];
		int _top;
	};
	void test1()
	{
		Stack<int, 10>  st1;//存储10个数据的静态栈
		Stack<int, 1000>  st1;//存储1000个数据的静态栈

	}
}

注意:C++20才开始支持double,int* ,现在只支持整型!!!

注意:

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

ok,接下来我们就来介绍一下array——

文档:array - C++ Reference

array是一个静态数组,它的底层使用了非类型模板参数,然后用这个非类型模板参数定义的数组。

1.1.1 常用接口

我们看到array的接口和前面我们所学容器的接口没有啥区别,唯一的区别就是:array不支持头插、尾插以及中间插入(这是因为空间已经开好了,无法进行扩容操作)

我们看到array是一个静态数组,那array和 int a[10] 有啥区别呢?array是封装的,而a不是封装的。

这里还有个一个问题:array支持的,数组a也支持啊, 那为什么要有array?

  • 我们可以更好的用这种类型(array),开空间更快,编译时给空间
代码语言:javascript
复制
	//假如现在我想在一个链表中中的每个节点中存一个数组,用array就很方便
	list<array<int, 10>> lt;
  • 传参数,数组传参是传指针,在函数的内部不能使用范围for,而array是引用传参。

如果数组想在函数中使用范围for,就要将数组的大小传进去。

除此之外,普通数组也可以使用sort,指向数组的指针是天然的迭代器!!!

总结:再去做其他容器类型,或者进行传参时,array都有普通数组达不到的优势!!!

在前面的学习中,我们了解到:数组越界是会检查的,但是这种检查是抽查,靠近临近位置可以查出来——

但是,这些问题对于array来说,简直就是小意思~

因为array是运算符重载调用,内存严格检查!!!

二、模板的定制能力:针对特定类型的特化实现

2.1 概念

所谓“特化”,其实就是特殊化处理

通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理,比如:实现了一个专门用来进行小于比较的函数模板 ok,举个例子:

代码语言:javascript
复制
#include<iostream>
#include<ostream>
using namespace std;
template<class T>
bool Less(T left, T right)
{
	return left < right;
}
class Date
{
public:
	Date(int year = 1, 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);
	}
	friend std::ostream& operator<<(std::ostream& out, const Date& d);
private:
	int _year;
	int _month;
	int _day;
};
std::ostream& operator<<(std::ostream& out, const Date& d)
{
	out << d._year << "/" << d._month << "/" << d._day;
	return out;
}
struct PDateLess
{
	bool operator() (const Date* p1, const Date* p2)
	{
		return *p1 < *p2;
	}
};
struct PDategreater
{
	bool operator() (const Date* p1, const Date* p2)
	{
		return *p1 > *p2;
	}
};
int main()
{
	Date* p1 = new Date(2025, 1, 1);
	Date* p2 = new Date(2025, 1, 2);
	cout << Less(p1, p2)<<endl;
	return 0;
}

但我们多次运行上面的代码,发现结果有时是1,有时又是0,上面的代码是按照指针进行大小的比较。

但是我们期望的不是按指针来比较,而是按照指针指向的内容来比较,那此时我们就可以实现一个函数模板的特化(对原版本的特殊化处理)

特化:针对某些类型进行特殊化处理!!!

ok,接下来,我们正式开始学习特化相关的知识~~~

2.2 函数模板特化

函数模板的特化步骤:

  1. 必须要先有一个基础的函数模板
  2. 关键字template后面接一对空的尖括号<>
  3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
  4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
代码语言:javascript
复制
// 函数模板 -- 参数匹配
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;
}

特化的特点:如果没有特化版本,就用类模板实例化出的;有特化版本就用特化版本!!!

特化并不像上面所写的这么简单,有些地方很复杂——

通过前面的学习,我们知道,我们应该加上const 引用,这样可以减少传值传参进行拷贝的操作

也就是说,我们应该这么写——

如果函数模板是这样的,那这个函数模板特化会很好写吗?

也许会有UU想说,这不是很简单嘛!直接将 T 换成 Date* 不就是实现针对 Date* 类型的特化了嘛——

好像有点不太对~

所以,我们应该让下面的const修饰引用,而不是修饰指针本身!!!

补充知识:在C++类型声明中,const 总是修饰它左边的内容

函数模板特化在有些地方还是有点麻烦的,通过前面的学习,我们知道模板和函数是可以一起存在的,那我们就可以不用写特化版本,可以直接写函数

2.3 类模板特化

类模板特化分为两种:

  1. 全特化
  2. 偏特化

类模板特化对内部成员没有要求,也就是原模板中定义的,特化版中可以不定义,也可以新增定义,可以认为类模板特化出的类是一个全新的类

2.3.1 全特化

所谓全特化,就是将模板参数列表中所有的参数都确定化

2.3.2 偏特化

偏特化也叫做半特化,也就是特化部分模板参数

  • 原模板:
代码语言:javascript
复制
//原模板
template<class T1,class T2>
class Date
{
public:
	Date()
	{
		cout << "Date<T1,T2>" << endl;
	}
private:
	T1 _d1;
	T2 _d2;
};

现在我想给T2确定为char类型,我就可以这样做——

当第二个模板参数是char时回去调用这个特化版本的模板

那这时候就有一个问题:当全特化和偏特化同时出现时,该怎么调用呢?

2.3.2.1 类模板特化应用示例:排序特化比较器实现

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

代码语言:javascript
复制
#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
复制
template<>
struct Less<Date*>
{
	bool operator()(Date* x,Date* y) const
	{
		return *x < *y;
	}
};
2.3.2.2 偏特化对参数的进一步限制

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

偏特化可以对参数进行进一步限制!!!

除了上面两种操作之外,还可以混着一起用——

三、模板分离编译

在这块内容里,我们就要来谈谈为什么说模板不能声明和定义分离到两个文件中(比如:一个在.cpp中,一个在.h中)

3.1 什么是分离编译

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

3.2 模板的分离编译

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

  • a.h
代码语言:javascript
复制
#include<iostream>
using namespace std;
template<class T>
T Add(const T& left, const T& right);
int add(int x, int y);
  • a.cpp
代码语言:javascript
复制
#define _CRT_SECURE_NO_WARNINGS
#include"a.h"
template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}
int add(int x, int y)
{
	return x + y;
}
代码语言:javascript
复制
// main.cpp
#include"a.h"
int main()
{
    add(1, 2);
    Add(1.0, 2.0);
    return 0;
}

当我们运行上面的代码时,会发现编译器会报出链接错误:

通常情况下,链接错误都是找不到定义

嗯?为什么不能找到这个模板中函数的定义呢?

这就需要我们对编译器的执行过程有一定的掌握:

所谓的链接错误,就是找不到这个函数的地址,那为什么链接时,普通add可以找到,模板函数Add就找不到了呢?

解决方法:

  • 可以显示实例化
代码语言:javascript
复制
#include"a.h"
template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}
//显示实例化
template<>
double Add(const double& left, const double& right)
{
	return left + right;
}

但是,如果类型是int呢?或者是其他类型呢?这样写是不是有点麻烦~

  • 方法2:直接在.h文件中定义(不将声明和定义分离)

直接在.h文件中定义,不要分离到两个文件中,就不会链接,本质是用的地方包含.h,直接就有函数模板的定义,直接实例化,编译直接拿到函数地址,不需要链接去找地址了

重点记住:模板不支持在两个文件中声明和定义分离!!!

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、非类型模板参数
    • 1.1 array——非类型模板参数的应用
      • 1.1.1 常用接口
  • 二、模板的定制能力:针对特定类型的特化实现
    • 2.1 概念
    • 2.2 函数模板特化
    • 2.3 类模板特化
      • 2.3.1 全特化
      • 2.3.2 偏特化
  • 三、模板分离编译
    • 3.1 什么是分离编译
    • 3.2 模板的分离编译
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档