【C++】模板进阶(非类型模板参数、类模板的特化和模板的分离编译)

news2024/10/5 21:22:42

     之前我们讲解过模板初阶,没有阅读过的童鞋可以先去阅读之前的博文----->模板初阶

本章我们将针对模板进行进一步的讲解。

目录

(一)非类型模板参数

(二)模板的特化

(1)概念

(2)函数模板的特化

(3)类模板的特化

1、全特化

2、偏特化(半特化)

 3、模板特化的应用

(三)模板分离编译


(一)非类型模板参数

非类型模板参数的引入:

  我们见一组样例,定义一个静态的栈:

#define N 100

template<class T>
class Stack
{
private:
	T _a[N];
	int _pop;
};

这里有个缺陷,这里的栈的大小是固定死的,每次创建出来的栈都是一样的大小。

那么问题来了怎么可以创建不同大小的数组:

模板让我们实现了同时可以创建不同数据类型的栈,但是不能实现不同数据个数的栈。

这里就引入了非类型模板参数:

//N是非类型模板参数,在类中当做常量来使用
//只能是整型常量
template<class T,int N=20>
class Array//库中也有array,array读写全面检查,检查更细致,但一般还是用vector
{
public:

private:
	T _a[N];
};
这里给出的N就是非类型模板参数,在类中可以当做常量来使用,N=20:20是给出一个缺省参数。(若用户给出N的大小则以用户的为主,若未给则用缺省参数)
特点:
  • N是常量不能修改
  • 模板不仅可以不定义类型,还可以定义常量
  • 比宏更好,可以传一个常量过去,定义不同的对象,可以传不同的常量过去

注意:
  • 1. 浮点数、类对象以及字符串不允许作为非类型模板参数的
  • 2. 非类型的模板参数必须在编译期就能确认结果
主要是区别类型形参和非类型形参!!!!   见下:
模板参数分类:
类型形参与非类型形参
  • 类型形参:出现在模板参数列表中,跟在class或者typename之类的参数类型名称
  • 非类型形参:就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用

(二)模板的特化

(1)概念

     通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理,比如:实现了一个专门用来进行小于比较的函数模板。
 
我们用日期类举例:
#include<iostream>
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);
	}

	friend ostream& operator<<(ostream& _cout, const Date& d)
	{
		_cout << d._year << "-" << d._month << "-" << d._day;
		return _cout;
	}

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

可以看到,Less绝对多数情况下都可以正常比较,但是在特殊场景下就得到错误的结果。上述示例中,p1指向的d1显然小于p2指向的d2对象,但是Less内部并没有比较p1和p2指向的对象内容,而比较的是p1和p2指针的地址,这就无法达到预期而错误。

所以针对日期类指针这种特殊的成员变量的比较,我们引入了模板的特化

在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化类模板特化。
对于上述日期类我们要给出他模板的特化:
这里有同学就要问了,一般情况下如果函数模板遇到不能处理或者处理有误的类型
,为了实现简单通常都是将该函数直接给出。然后用 函数重载即可。
所以我们不常用函数模板特化这种方法!

 什么时候该用特化呢?

其实模板特化中分为函数模板特化类模板特化。我们下面分开讲解:

(2)函数模板的特化

函数模板的 特化步骤:
  • 1. 必须要先有一个基础的函数模板
  • 2. 关键字template后面接一对空的尖括号<>
  • 3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
  • 4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
// 函数模板 -- 参数匹配
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;
}
注意:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给 出。
直接函数重载:

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

(3)类模板的特化

既然上面函数模板的特化不建议使用,那么类模板的特化必将有大作用。

1、全特化

全特化即是将模板参数列表中所有的参数都确定化
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; }
};
  • 制定好了两个模板参数 —— 写死了
  • 只能固定的类型走这个函数
  • 例如:只能是int 和 double走全特化的模板

2、偏特化(半特化)

偏特化:任何针对模版参数进一步进行条件限制设计的特化版本。
偏特化有以下两种表现方式:
部分特化     将模板参数类表中的一部分参数特化   
参数更进一步的限制    偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。
比如对于以下模板类:
//半特化 / 偏特化()半特化不是特化一半
//1、将部分模板参数列表中的一部分参数特化
template<class T1>
class Data<T1, char>
{
public:
	Data() { cout << "Data<T1, char>" << endl; }
};

//2、偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本
//只要T1 和 T2是指针就走这个 -- 针对指针特殊化处理
template<class T1, class T2>
class Data<T1*, T2*>
{
public:
	Data() { cout << "Data<T1*, T2*>" << endl; }
};

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

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

	//只要第二个是char都会匹配:半特化/偏特化
	Data<int, char> d3;
	Data<char, char> d4;

	//只要是两个指针
	Data<int*, int*> d5;
	Data<int*, char*> d6;
	Data<int*, string*> d7;
	Data<int*, void*> d8;
	//void不是类型,但是void*是一个类型,void*是不能解引用不能++

	Data<int*, int> d9;//匹配原生的指针
	Data<int&, char&> d10;

	return 0;
}



//也可以写成typename
//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 = T1(), const T2& d2 = T2())
//		: _d1(d1)
//		, _d2(d2)
//	{
//		cout << "Data<T1&, T2&>" << endl;
//	}
//
//private:
//	const T1& _d1;
//	const T2& _d2;
//};
//
//template <typename T1, typename T2>
//class Data <T1&, T2*>
//{
//public:
//	Data(const T1& d1 = T1())
//
//	{
//		cout << "Data<T1&, T2*>" << endl;
//	}
//};
//int main()
//{
//	Data<int, int> d1;
//	Data<int*, char*> d2;
//	Data<int*, int> d3;
//	Data<double, int> d4;
//
//	Data<int*, int*> d5;
//
//	Data<int&, char&> d6;
//	Data<int&, char*> d7;
//
//	return 0;
//}
//

运行结果如下:

 3、模板特化的应用

有如下专门用来按照小于比较的类模板Less:
//模板的特化 -- 应用
template<class T>
struct Less
{
	bool operator()(const T& x, const T& y) const
	{
		return x < y;
	}
};

template<>
struct Less<Date*>
{
	bool operator()(Date* x, Date* y) const
	{
		return *x < *y;
	}
};

//偏特化
//只要是指针都走这里
template<class T>
struct Less<T*>
{
	bool operator()(T* x, 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*>());

	vector<int*> v3;
	v3.push_back(new int(3));
	v3.push_back(new int(1));
	v3.push_back(new int(2));
	sort(v3.begin(), v3.end(), Less<int*>());

	return 0;
} 

(三)模板分离编译

1、 什么是分离编译
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。
假如有以下场景,模板的声明与定义分离开,在头文件中进行声明,源文件中完成定义:
// 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(1.0, 2.0);
 
 return 0;
}

原因是:分离的话,模板实例化不出对应的函数,但是编译时可以通过的,因为声明中有模板的声明,最后符号表重定位的时候,找不到对应的函数模板调用的地址。

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

补充:

  • 模板不支持声明和定义分别放到xxx.h和xxx.cpp中
  • 一般是要放到一个文件中。有些地方就会命名成
  • xxx.hpp,寓意就是头文件和定义实现内容合并一起.
  • 但是并不是必须是.hpp, .h也是可以的

解决方案1:在template.cpp中针对于要使用的模板类型显示实例化(指定类型)

 解决方案2:在不要分离到两个文件中。直接写在xxx.hpp或xxx.h中,也就是声明和定义不分离

这样就能将函数实例化出来,在编译的时候就能call这个函数的地址了,就不需要链接的时候去找了。

感谢阅读,助您学业有成!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/413251.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

我的面试八股(Java集合篇)

Java集合 两个抽象接口派生&#xff1a;一个是Collection接口,存放单一元素&#xff1b;一个是Map接口存放键值对。 Vector为什么是线程安全 简单&#xff0c;因为官方在可能涉及到线程不安全的操作都进行了synchronized操作&#xff0c;就自身源码就给你加了把锁。 Vector…

Stacking:解决机器学习进行多模型组合的实用工具

文章目录1 Stacking原理第一步&#xff1a;生成预测结果第二步&#xff1a;整合预测结果2 使用Python实现Stacking第一步&#xff1a;生成预测结果第二步&#xff1a;整合预测结果借助sklearn实现stacking3 各领域内的一些实际应用在机器学习领域&#xff0c;算法的选择和参数的…

前端--文件上传--文件切片--利用FileReader()中的readAsDataURL()做缩略图--多文件上传--formData--切片上传实现

一、文件上传 <template><div><input type"file" name"file" change"fileChange" /><button click"submit">提交</button></div> </template><script setup>function fileChange(e)…

ROS开发之如何制作launch启动文件?

文章目录0、引言1、Launch文件语法2、Launch示例0、引言 笔者因研究课题涉及ROS开发&#xff0c;学习了古月居出品的ROS入门21讲&#xff0c;为巩固launch的知识&#xff0c;本文将ROS的launch启动文件制作一讲内容进行总结。launch文件通过XML文件实现多节点的配置和启动&…

Compose (11/N) - 手势

一、点击 1.1 可点击 Modifier.clickable( ) 允许应用检测对该元素的点击。 Composable fun ClickableSample() {val count remember { mutableStateOf(0) }Text(text count.value.toString(),modifier Modifier.clickable { count.value 1 }) } 1.2 手势检测 Modifier.p…

【技术分享】接口自动化测试中,如何做断言验证?

在服务端自动化测试过程中&#xff0c;发起请求之后还需要对响应值进行验证。验证响应信息符合预期值之后&#xff0c;这一条接口自动化测试用例才算完整的通过。所以这一章节&#xff0c;将会讲解在接口自动化测试中&#xff0c;如何对服务端返回的响应内容做断言验证。 实战…

C语言函数大全-- i 开头的函数

C语言函数大全 本篇介绍C语言函数大全– i 开头的函数 1. imagesize 1.1 函数说明 函数声明函数功能unsigned imagesize(int left, int top, int right, int bottom);获取保存位图像所需的字节数 1.2 演示示例 #include <graphics.h> #include <stdlib.h> #in…

【Java数据结构】链表OJ提交小记

目录 1.删除链表中所有值为val的节点 2.反转单链表 3.返回链表的中间节点 4.返回链表倒数第k个节点 5.按次序合并链表 6.按值分割链表 7.判断链表是否为回文 1.删除链表中所有值为val的节点 1. 删除链表中所有值为val的节点https://leetcode.cn/problems/remove…

考研数据结构-绪论

绪论 文章目录绪论1. 什么是数据结构2. 基本概念数据结构的四类基本结构&#xff08;逻辑结构&#xff09;存储结构顺序存储和链式存储比较分析3. 算法概念特征优点&#xff08;也是要求&#xff09;算法效率的度量概念时间复杂度空间复杂度(了解)1. 什么是数据结构 数据结构是…

【Python】字符串 ⑦ ( input 字符串输入 | input 函数自带提示参数 | input 函数接收的变量类型 )

文章目录一、input 字符串输入二、代码示例三、input 函数自带提示参数四、input 函数接收的变量类型一、input 字符串输入 在命令行中 , 使用 printf 可以输出数据 , 将 变量 , 字面量 , 表达式 输出到命令行中 ; 在命令行中 , 使用 input 语句可以 在 命令行 中 , 从键盘获取…

2023年第五届传智杯前四题题解(后俩没写出来)

比赛链接&#xff1a;第五届“传智杯”全国大学生计算机大赛&#xff08;决赛B组&#xff09; - 比赛详情 - 洛谷 时效「月岩笠的诅咒」 题目背景 蓬莱之药&#xff0c;被诅咒的不死之药。 奉命将蓬莱之药投入富士山中销毁的月岩笠&#xff0c;最终打算把蓬莱之药改投入八岳销…

STM32Cube的debug和release切换

一&#xff0c; Debug / Release版本区别 来源&#xff1a;STM32CUBEIDE中 Debug 和 Release 的作用/区别/使用场景 - svchao - 博客园 (cnblogs.com) 二&#xff0c;Debug / Release使用。 1&#xff0c;在编译的时候可以选择Debug 还是 Release . 2,使用stm32CubeIDE调试或运…

【模型复现】resnet,使用net.add_module()的方法构建模型。小小的改进大大的影响,何大神思路很奇妙,基础很扎实

从经验来看&#xff0c;网络的深度对模型的性能至关重要&#xff0c;当增加网络层数后&#xff0c;网络可以进行更加复杂的特征模式的提取&#xff0c;所以当模型更深时理论上可以取得更好的结果。但是更深的网络其性能一定会更好吗&#xff1f;实验发现深度网络出现了退化问题…

Git的安装与基本使用

Git是一个分布式版本控制工具&#xff0c;可以快速高效地处理从小型到大型的各种项目。 1.Git的安装 官网下载地址 &#xff1a;https://git-scm.com/ 安装过程 选择 Git 安装位置&#xff0c;要求是非中文并且没有空格的目录&#xff0c;然后下一步。 Git 选项配置&#xf…

ChatGPT搭建语音智能助手

环境 python&#xff1a;3 ffmpeg:用于处理视频和语音 gradio:UI界面和读取语音 概述 我们的目的是做一个语音智能助手 下面我们开始 准备工作 下载Visual Studio Code Visual Studio Code 因为需要写python代码&#xff0c;用Visual Studio Code比较方便。 安装pytho…

( “树” 之 DFS) 101. 对称二叉树 ——【Leetcode每日一题】

101. 对称二叉树 给你一个二叉树的根节点 root &#xff0c; 检查它是否轴对称。 示例 1&#xff1a; 输入&#xff1a;root [1,2,2,3,4,4,3] 输出&#xff1a;true 示例 2&#xff1a; 输入&#xff1a;root [1,2,2,null,3,null,3] 输出&#xff1a;false 提示&#xff1a…

webgl-画任意多边形

注意&#xff1a; let canvas document.getElementById(webgl) canvas.width window.innerWidth canvas.height window.innerHeight let radio window.innerWidth/window.innerHeight; let ctx canvas.getContext(webgl) 由于屏幕长宽像素不一样&#xff0c;导致了长宽像素…

移远云服务QuecCloud正式发布,一站式为全球客户提供创新有效的解决方案

4月12日&#xff0c;在“万物智联共数未来”移远通信物联网生态大会上&#xff0c;移远通信宣布正式推出其物联网云服务——QuecCloud。QuecCloud具备智能硬件开发、物联网开放平台、行业解决方案三大能力&#xff0c;可为开发者和企业用户提供从硬件接入到软件应用的全流程解决…

Java 进阶(5) Java IO流

⼀、File类 概念&#xff1a;代表物理盘符中的⼀个⽂件或者⽂件夹。 常见方法&#xff1a; 方法名 描述 createNewFile() 创建⼀个新文件。 mkdir() 创建⼀个新⽬录。 delete() 删除⽂件或空⽬录。 exists() 判断File对象所对象所代表的对象是否存在。 getAbsolute…

4.2 方差

学习目标&#xff1a; 我认为学习方差需要以下几个步骤&#xff1a; 确定学习目标&#xff1a;在开始学习方差之前&#xff0c;需要明确学习的目标和意义&#xff0c;例如&#xff0c;理解方差的定义、掌握方差的计算方法、了解方差在实际问题中的应用等。 学习相关数学概念&…