【C++】模板进阶--非类型模板参数模板特化及分离编译

文章目录

  • 一、非类型模板参数
  • 二、模板的特化
    • 1.模板特化的概念
    • 2.函数模板的特化
    • 3.类模板的特化
      • 3.1 全特化
      • 3.2 偏特化
    • 4.类模板特化应用示例
  • 三、模板的分离编译
  • 四、模板总结

一、非类型模板参数

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

我们以定义一个静态的数组为例,在没有非类型模板参数的时候,我们只能采用如下的方式来定义一个静态的数组:

#define N 10
template<class T>
class arr
{
public:
	// ...
private:
	T _a[N];
};
void test()
{
    arr<int> arr;
}

但是这样定义一个数组有一个极大的缺陷,那就是当我们同时需要使用不同大小的数组的时候,就无法实现,针对这个问题,C++设计出了非类型模板参数来解决这个问题,非类型模板参数和类型模板参数一样,也是通过传递不同的非类型模板参数来定义不同的类,代码如下:

template<class T,size_t N>
class arr
{
public:
	// ...
private:
	T _a[N];
};
void test()
{
	arr<int, 10> arr1;
	arr<int, 100> arr2;
}

【注意】

1.非类型模板参数可以给缺省值

2.浮点数、类对象以及字符串是不允许作为非类型模板参数的,非类型模板参数只能是整形

3.非类型的模板参数必须在编译期就能确认结果,即非类型模板参数的实参只能是一个常量

二、模板的特化

1.模板特化的概念

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

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

在这里插入图片描述

我们可能会说,我们为什么不直接重载一个Date*的函数呢,而是大费周章采用模板特化呢,确实是这样的,因为函数支持多个重载函数,所以注意:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出

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

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

3.类模板的特化

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, char>
{
public:
	Data()
    {
        cout << "Data<int, char>" << endl;
    }
private:
	int _d1;
	char _d2;
};
void TestVector()
{
	Data<int, int> d1;
	Data<int, char> d2;
}

在这里插入图片描述

3.2 偏特化

偏特化:任何针对模版参数进一步进行条件限制设计的特化版本。比如对于以下模板类

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

偏特化有以下两种表现方式:

1.部分特化

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

// 将第二个参数特化为int
template <class T1>
class Data<T1, int>
{
public:
    Data()
    {
        cout<<"Data<T1, int>" <<endl;
    }
private:
    T1 _d1;
    int _d2;
};
void TestVector()
{
    // 第二个参数与模板特化中的特化参数相同,优先使用特化模板进行实例化
	Data<int, int> d1;
    // 使用普通模板实例化
	Data<int, char> d2;
}

在这里插入图片描述

我们可以看到,我们将模板的部分参数显示指定为某种具体的类型,这样模板参数进行匹配的时候会优先匹配

2.参数更进一步的限制

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

//两个参数偏特化为指针类型
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;
};
void test()
{
	Data<double, int> d1; // 调用特化的int版本
	Data<int, double> d2; // 调用基础的模板 
	Data<int*, int*> d3; // 调用特化的指针版本
	Data<int&, int&> d4(1, 2); // 调用特化的指针版本
}

在这里插入图片描述

从上面的结果,我们可以看到,通过偏特化对模板参数进行进一步限制,比如将模板参数定义为<T*,T*>,这样不管是任何类型的指针都会调用该特化模板,从而实现了在限制参数类型的同时又不会将参数局限为某一具体类型

4.类模板特化应用示例

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

#include<vector>
#include <algorithm>
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>
struct Less
{
	bool operator()(const T& x, const T& y) const
	{
		return x < y;
	}
};
void test()
{
	Date d1(2023, 4, 3);
	Date d2(2023, 4, 1);
	Date d3(2023, 4, 2);
	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 << endl;
	}
	cout << endl;
	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*>());
	for (auto e : v2)
	{
		cout << *e << endl;
	}
	cout << endl;
}

在这里插入图片描述

通过观察上述程序的结果发现,对于日期对象可以直接排序,并且结果是正确的。但是如果待排序元素是指针,结果就不一定正确。因为:sort最终按照Less模板中方式比较,所以只会比较指针,而不是比较指针指向空间中内容,此时可以使用类版本特化来处理上述问题:

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

在这里插入图片描述

三、模板的分离编译

1.什么是分离编译

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

2.模板的分离编译

我们以stack为例:

stack.h

#pragma once
#include<iostream>
using namespace std;

template<typename T>
class Stack
{
public:
	Stack(int capacity = 4);
	~Stack();
	void Push(const T& x);

private:
	T* _a;
	int _top;
	int _capacity;
};

stack.cpp

#include "Stack.h"

template<class T>
Stack<T>::Stack(int capacity)
{
	cout << "Stack(int capacity = )" << capacity << endl;

	_a = (T*)malloc(sizeof(T)*capacity);
	if (_a == nullptr)
	{
		perror("malloc fail");
		exit(-1);
	}

	_top = 0;
	_capacity = capacity;
}

template<class T>
Stack<T>::~Stack()
{
	cout << "~Stack()" << endl;

	free(_a);
	_a = nullptr;
	_top = _capacity = 0;
}

template<class T>
void Stack<T>::Push(const T& x)
{
	// ....
	// 
	_a[_top++] = x;
}

test.cpp

#include "stack.h"

int main()
{
	Stack<int> st;
	st.Push(1);
	st.Push(2);
	st.Push(3);
	return 0;

在这里插入图片描述

当我们运行的时候发现出现了链接性错误;造成这种的原因如下:

我们知道,一个.c/.cpp程序变成.exe可执行程序一共需要经历四个步骤,分别是预处理,编译,汇编和链接,在这个过程中,他们所执行的工作为:

预处理:注释的删除,#define定义的符号,宏的替换以及删除,各种条件编译的处理,头文件的展开

编译:进行词法分析,语法分析,语义分析和符号汇总

汇编:生成符号表

链接:合并段表,符号表的合并和重定位

此外,预处理,编译,汇编这几个阶段每个源(.c文件)文件都是独立进行的,只有在链接的时候才会将这几个目标文件合并到一起形成可执行程序

在这里插入图片描述

综上所诉,我们可知程序报错的原因如下:

1.预处理时,stack.h头文件分别在stack.c和test.c源文件展开

2.经过编译,stack.cpp和test.cpp分别转变成汇编代码

3.经过汇编,stack.cpp里面有stack,push函数的声明,但是没有他们的定义,所以test.cpp在生成符号表的时候会给这些函数一个无效的地址,此外,由于stack.cpp里面没有对函数模板实例化的代码,即没有stack,也就没有生成具体的代码,即没有stack的定义,所以stack.cpp的符号表里函数对应的地址是一个无效的地址

4.在链接时,需要将test.cpp和stack.cpp符号表的内容进行合并与重定位,但由于他们符号表中都是无效的地址,所以会发生链接错误

我们了解程序报错的原因之后,想到的第一个解决方案应该是在stack.cpp中对模板进行实例化

// 在stack.cpp中增加对stack的显式实例化
template class Stack<int>;

这样的做法确实能够解决这个问题,但是当我们定义一个存放double类型的栈的时候,此时,我们又需要在stack.cpp中对stack再进行实例化一次,如果我们再定义不同的对象,就需要不断的进行显式实例化,也就是说,在同一份代码中我们只能定义同一类类型的对象,那么这样就十分麻烦,也失去了模板的初衷了,所以模板不支持分离编译,我们一般采用如下的方法解决:

1.将声明和定义放到一个文件"xxx.hpp" 里面或者xxx.h**其实也是可以的

2.模板定义的位置显式实例化。这种方法不实用,不推荐使用

【分离编译扩展阅读】为什么C++编译器不能支持对模板的分离式编译

但是声明解决的两种方式有一个问题,就是将函数的声明和定义放在同一个文件中,我们就将类提供给别人使用时,也将底层实现也暴露了给别人

四、模板总结

模板优点

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

2.增强了代码的灵活性

模板缺点

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Spring启动及Bean实例化过程来看经典扩展接口

目录 一、Spring启动及Bean实例化过程 二、分析其对应经典扩展接口 三、对开发的指导意义 备注&#xff1a;以下总结只是一些基本的总结思路&#xff0c;具体每个扩展接口的应用后续进行分析总结。 一、Spring启动及Bean实例化过程 Spring启动及Bean实例化的过程&#xff0…

CSS实现三角形的四种方法

方法一&#xff1a;使用 border (常见) 【解释】不设置宽高&#xff0c;用边框大小控制三角型大小 【分解步骤】 设置一个div不设宽高 【示例】 <style>#triangle{width: 0;height: 0;border: 100px solid;border-color: orangered skyblue gold yellowgreen;} </s…

Python第三方库安装

看见更大的Python世界 Python社区PyPI The Python Package Index PyPI: Python Package Index PSF维护的展示全球Python计算生态的主站 学会检索并利用PyPI&#xff0c;找到合适的第三方库开发程序 实例&#xff1a;开发与区块链相关的程序 第1步&#xff1a;在pypi.org…

第01章_Java语言概述

第01章_Java语言概述 讲师&#xff1a;尚硅谷-宋红康&#xff08;江湖人称&#xff1a;康师傅&#xff09; 官网&#xff1a;http://www.atguigu.com 1. Java知识脉络图 1.1 Java基础全程脉络图 1.2 本章专题与脉络 2. 抽丝剥茧话Java 2.1 当前大学生就业形势 麦可思研究院…

fMRI研究 | 社交情境下的混合情绪

导读 背景&#xff1a;神经科学通常都是单独研究各种情绪&#xff0c;而混合的情绪状态&#xff08;例如愉悦和厌恶、悲伤和快乐的共存&#xff09;在日常生活中很常见。心理生理学和行为学证据表明&#xff0c;混合情绪可能具有不同于其组成情绪的反应特征。然而&#xff0c;…

PHP快速入门02-PHP语言基础

文章目录前言一、 数据类型1.1 String&#xff08;字符串&#xff09;1.2 Integer&#xff08;整型&#xff09;1.3 Float&#xff08;浮点型&#xff09;1.4 Boolean&#xff08;布尔型&#xff09;1.5 Array&#xff08;数组&#xff09;1.6 Object&#xff08;对象&#xff…

重感知还是重地图?其实无需选择

近来&#xff0c;关于自动驾驶应该重感知还是重地图是个热点话题&#xff0c;很多重量级车厂、自动驾驶供应商都开始提出重感知轻地图的方案&#xff0c;并承诺很快能发布出对应的产品。业界也出现了高精地图已“死”等类似的言论。 一时之间&#xff0c;似乎轻地图已经成为了…

SpringBoot整合Redis、以及缓存穿透、缓存雪崩、缓存击穿的理解、如何添加锁解决缓存击穿问题?分布式情况下如何添加分布式锁

文章目录1、步骤2、具体过程1、引入pom依赖2、修改配置文件3、单元测试4、测试结果3、redis运行情况4、项目中实际应用5、加锁解决缓存击穿问题代码一&#xff08;存在问题&#xff09;代码二&#xff08;问题解决&#xff09;6、新问题7、分布式锁1、步骤 前提条件&#xff1…

Attention is all your need——Transformer论文

摘要 此序列转录模型仅仅依赖于注意力机制&#xff0c;而不使用循环或者是卷积&#xff0c;将循环全部换成了multi-headed self- attention 介绍 RNN的特点、并行程度低。 Attention在RNN上的应用。 引入注意力机制&#xff0c;提高并行度。 背景 使用卷积对长的序列难以…

3D开发工具HOOPS最新解析合集,助力实现web端高性能模型渲染

一、3D技术为创新提供强大助力 不管您想搭建桌面、WEB或者移动端APP应用&#xff0c;技术领先全球的HOOPS Platform组件都可以为您提供弹性的3D集成架构&#xff0c;同时&#xff0c;一批可信任的工业领域3D技术专家也将为您提供技术支持服务。 如果您的客户期望有一种在多个…

第三十一天 Linux介绍和基础命令

目录 1.前言 1.1 什么是Linux 1.2 为什么要学Linux 1.3 学完Linux能干什么 2.Linux简介 2.1 主流操作系统 2.2 Linux发展历史 3. Linux安装 3.1 安装方式介绍 3.2 安装VMware 3.3 安装Linux 3.4 网卡设置 3.5 安装SSH连接工具 3.6 Linux目录结构 4.Linux常用命令…

DHTMLX Gantt入门使用教程【引入】:如何开始使用 dhtmlxGantt

DHTMLX Gantt是用于跨浏览器和跨平台应用程序的功能齐全的Gantt图表。可满足项目管理应用程序的大部分开发需求&#xff0c;具备完善的甘特图图表库&#xff0c;功能强大&#xff0c;价格便宜&#xff0c;提供丰富而灵活的JavaScript API接口&#xff0c;与各种服务器端技术&am…

Excel玩转自然语言查询

ChatGPT火出圈&#xff0c;人类被人工智能替代又成为热门话题。有人欢喜&#xff0c;有人忧&#xff0c;也有人不以为意&#xff0c;觉得离自己工作远着呢&#xff0c;比如现在是用Excel做报表&#xff0c;有本事你动动嘴就直接把Excel里面的数据查询出来啊。 你可别说&#xf…

【题解】P4055 [JSOI2009] 游戏

link 题目大意 题目说得比较清楚。 题解 前置知识&#xff1a;二分图最大匹配、基础博弈论。 每个点只能走一次的四联通点阵&#xff0c;可以想到二分图匹配。 将其套路地奇偶分点&#xff0c;相邻两点连边&#xff08;显然不能为 #&#xff09;。 先求一个最大匹配。 …

5G/V2X赛道「重启」

在提升高阶智能驾驶安全性和感知冗余能力的道路上&#xff0c;除了激光雷达、高精度地图及定位&#xff0c;还有一项技术可能即将掀起一场新的风暴。 就在今年3月&#xff0c;作为全球通信领域的年度风向标 — 2023世界移动通信大会&#xff08;MWC&#xff09;上&#xff0c;…

基于html+css的盒子展示6

准备项目 项目开发工具 Visual Studio Code 1.44.2 版本: 1.44.2 提交: ff915844119ce9485abfe8aa9076ec76b5300ddd 日期: 2020-04-16T16:36:23.138Z Electron: 7.1.11 Chrome: 78.0.3904.130 Node.js: 12.8.1 V8: 7.8.279.23-electron.0 OS: Windows_NT x64 10.0.19044 项目…

第七回:如何使用GirdView Widget

文章目录概念介绍使用方法示例代码经验总结我们在上一章回中介绍了Image Widget,本章回中将介绍 GirdView这种Widget&#xff0c;闲话休提&#xff0c;让我们一起Talk Flutter吧。概念介绍 在Flutter中使用GirdView表示网格状的布局&#xff0c;类似日常办公中使用的Excel,它和…

win10彻底永久关闭自动更新【亲测有效】

一、禁用Windows Update服务 1、同时按下键盘 Win R&#xff0c;打开运行对话框&#xff0c;然后输入命令 services.msc &#xff0c;点击下方的“确定”打开服务&#xff0c;如下图所示。 2、找到 Windows Update 这一项&#xff0c;并双击打开&#xff0c;如图所示。 3、右击…

MySQL-中间件mycat(二)

目录 &#x1f341;部署主从复制 &#x1f341;mycat读写分离 &#x1f342;修改配置文件 &#x1f342;设置balance与writeType &#x1f342;设置switchType与slaveThreshold &#x1f342;启动程序 &#x1f342;验证读写分离 &#x1f341;垂直拆分-分库 &#x1f342;实现…

openvpn (用户名密码模式)

目录 一、介绍 1、定义 2、原理 3、加密和身份验证 二、在centos 7.5上搭建openvpn 1、安装openvpn 和easy-rsa&#xff08;该包用来制作ca证书&#xff09; 2、配置/etc/openvpn/ 目录 3、创建服务端证书及key 4、创建客户端证书 5、把服务器端必要文件放到etc/openvpn/ 目录下…