泛型编程——模板详解

news2024/11/17 8:48:09

文章目录

  • 1. 前言
  • 2. 泛型编程
  • 3. 函数模板
    • 3.1 函数模板的概念
    • 3.2 函数模板的格式
    • 3.3 函数模板的原理
    • 3.4 函数模板的实例化
    • 3.5 模板参数的匹配原则
  • 4. 类模板
    • 4.1 类模板的定义格式
    • 4.2 类模板的实例化
  • 5. 模板参数
  • 6. 模板的特化
    • 6.1 概念
    • 6.2 函数模板特化
    • 6.3 类模板特化
      • 6.3.1 全特化
      • 6.3.2 偏特化
      • 6.3.3 类模板特化应用示例
  • 7. 模板分离编译
    • 7.1 什么是分离编译
    • 7.2 模板的分离编译
    • 7.3 解决方法
  • 8. 模板的优缺点


1. 前言

当涉及到泛型编程和代码复用时,C++ 模板是一项强大的特性,它可以在编译时实现类型安全的代码生成。通过使用模板,我们可以编写通用的代码,以适应多种数据类型和算法的需求,从而提高代码的可复用性和灵活性。在本篇文章中,我们将探索函数模板、类模板、模板参数、模板的特化以及模板的分离编译的知识。

2. 泛型编程

如何实现一个通用的交换函数呢?

void Swap(int& left, int& right)
{
	int temp = left;
	left = right;
	right = temp;
}
void Swap(double& left, double& right)
{
	double temp = left;
	left = right;
	right = temp;
}
void Swap(char& left, char& right)
{
	char temp = left;
	left = right;
	right = temp;
}

使用函数重载虽然可以实现,但是有以下几个不好的地方:

  1. 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数

  2. 代码的可维护性比较低,一个出错可能所有的重载均出错

那能否告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码呢?

如果在C++中,也能够存在这样一个模具,通过给这个模具中填充不同材料(类型),来获得不同材料的铸件(即生成具体类型的代码),那将会节省许多头发。巧的是前人早已将树栽好,我们只需在此乘凉。

C++ 泛型编程是一种编程范式,它通过使用模板来实现通用的代码,可以在不同的数据类型上进行操作和处理。泛型编程的目标是提高代码的可重用性、灵活性和性能。

在这里插入图片描述

3. 函数模板

3.1 函数模板的概念

函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。

3.2 函数模板的格式

template<typename T1, typename T2,......,typename Tn>
返回值类型 函数名(参数列表){}

因此,通用交换函数就可以这样实现:

template<typename T>
void Swap(T& left, T& right)
{
	T temp = left;
	left = right;
	right = temp;
}

注意:typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替class)

3.3 函数模板的原理

函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器。

图解:

在这里插入图片描述

文字解释

在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。

3.4 函数模板的实例化

用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。

  1. 隐式实例化:让编译器根据实参推演模板参数的实际类型
  2. 显式实例化:在函数名后的<>中指定模板参数的实际类型
template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}
int main()
{
	int a1 = 10, a2 = 20;
	double d1 = 10.0, d2 = 20.0;
	Add(a1, a2);
	Add(d1, d2);
	/*
	Add(a1, d1);
	该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型
	通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有一个T,
	编译器无法确定此处到底该将T确定为int 或者 double类型而报错
	注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅
	*/
	// 此时有两种处理方式:
	Add(a1, (int)d1); // 1. 用户自己来强制转化
	Add<int>(a1, d1); // 2. 使用显式实例化
	return 0;
}

注:如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。

3.5 模板参数的匹配原则

  1. 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数

例如:

// 专门处理int的加法函数
int Add(int left, int right)
{
	return left + right;
}
// 通用加法函数
template<class T>
T Add(T left, T right)
{
	return left + right;
}
void Test()
{
	Add(1, 2); // 与非模板函数匹配,编译器不需要特化
	Add<int>(1, 2); // 调用编译器特化的Add版本
}
  1. 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板

例如:

// 专门处理int的加法函数
int Add(int left, int right)
{
	return left + right;
}
// 通用加法函数
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
	return left + right;
}
void Test()
{
	Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化
	Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数
}
  1. 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换

4. 类模板

4.1 类模板的定义格式

template<class T1, class T2, ..., class Tn>
class 类模板名
{
	// 类内成员定义
};

实例理解

// 动态顺序表
// 注意:Vector不是具体的类,是编译器根据被实例化的类型生成具体类的模具
template<class T>
class Vector
{
public:
	Vector(size_t capacity = 10)
		: _pData(new T[capacity])
		, _size(0)
		, _capacity(capacity)
	{}
	// 使用析构函数演示:在类中声明,在类外定义。
	~Vector();
	void PushBack(const T& data)void PopBack()// ...
	size_t Size() { return _size; }
	T& operator[](size_t pos)
	{
		assert(pos < _size);
		return _pData[pos];
	}
private:
	T* _pData;
	size_t _size;
	size_t _capacity;
};
// 注意:类模板中函数放在类外进行定义时,需要加模板参数列表
template <class T>
Vector<T>::~Vector()
{
	if (_pData)
		delete[] _pData;
	_size = _capacity = 0;
}

4.2 类模板的实例化

类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类

例如:

// Vector类名,Vector<int>才是类型
Vector<int> s1;
Vector<double> s2

5. 模板参数

模板参数分为类型参数与非类型参数。

  • 类型参数:类型参数是指在模板中使用的表示类型的参数。类型参数可以用作函数模板的参数类型或类模板的成员类型。它们用于定义通用的代码,可以在编译时根据具体的类型进行实例化。类型参数可以是任何合法的 C++ 类型,包括基本类型、自定义类型、指针类型、引用类型等。类型参数使用关键字 “typename” 或 “class” 来声明。

    例如:

    template <typename T>
    void Print(T value) {
        // 函数模板的代码逻辑
    }
    
    template <typename T>
    class MyContainer {
        // 类模板的成员和代码逻辑
    };
    
  • 非类型参数:非类型参数是指在模板中使用的表示常量值的参数。非类型参数可以是整数、枚举、指针或引用类型的常量表达式。它们用于在编译时计算常量值,并将该值绑定到模板中的特定参数。非类型参数在模板实例化时被确定,并在编译期间进行求值。非类型参数使用特定的类型或值来声明。

    例如:

    template <int N>
    void PrintArraySize(int (&arr)[N]) {
        // 函数模板的代码逻辑
    }
    
    template <typename T, int Size>
    class FixedSizeArray {
        T data[Size];
        // 类模板的成员和代码逻辑
    };
    

注意:

  1. 浮点数、类对象以及字符串是不允许作为非类型模板参数的。

  2. 非类型的模板参数必须在编译期就能确认结果。

6. 模板的特化

6.1 概念

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

// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
	return left < right;
}
int main()
{
	cout << Less(1, 2) << endl; // 可以比较,结果正确
	Date d1(2024, 1, 17);
	Date d2(2024, 7, 18);
	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指针的地址,这就无法达到预期而错误。

此时,就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化类模板特化

6.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(2024, 1, 17);
	Date d2(2024, 1, 18);
	cout << Less(d1, d2) << endl;
	Date* p1 = &d1;
	Date* p2 = &d2;
	cout << Less(p1, p2) << endl; // 调用特化之后的版本,而不走模板生成了
	return 0;
}

注意:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。

像这样:

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

该种实现简单明了,代码的可读性高,容易书写,因此函数模板不建议特化。

6.3 类模板特化

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

6.3.2 偏特化

偏特化:任何针对模版参数进一步进行条件限制设计的特化版本。

比如对于以下模板类:

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

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

  • 部分特化

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

    // 将第二个参数特化为int
    template <class T1>
    class Data<T1, int>
    {
    public:
    	Data() { cout << "Data<T1, int>" << endl; }
    private:
    	T1 _d1;
    	int _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;
    };
    void test2()
    {
    	Data<double, int> d1; // 调用特化的int版本
    	Data<int, double> d2; // 调用基础的模板
    	Data<int*, int*> d3; // 调用特化的指针版本
    	Data<int&, int&> d4(1, 2); // 调用特化的指针版本
    }
    

6.3.3 类模板特化应用示例

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

#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(2024, 1, 17);
	Date d2(2024, 1, 16);
	Date d3(2024, 1, 18);
	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模板中方式比较,所以只会比较指针,而不是比较指针指向空间中内容,此时可以使用类版本特化来处理上述问题:

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

特化之后,在运行上述代码,就可以得到正确的结果。

7. 模板分离编译

7.1 什么是分离编译

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

7.2 模板的分离编译

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

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

图解分析:

在这里插入图片描述

7.3 解决方法

  1. 将声明和定义放到一个xxx.hpp或者xxx.h文件里其实是可以的。推荐使用这种。

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

8. 模板的优缺点

【优点】

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

  2. 增强了代码的灵活性。

【缺点】

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

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

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

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

相关文章

【STM32】STM32学习笔记-I2C通信外设(34)

00. 目录 文章目录 00. 目录01. I2C简介02. I2C外设简介03. I2C框图04. I2C基本结构05. 主机发送06. 主机接收07. 软件/硬件波形对比08. 预留09. 附录 01. I2C简介 I2C(Inter&#xff0d;Integrated Circuit)总线是一种由NXP&#xff08;原PHILIPS&#xff09;公司开发的两线式…

链表练习 Leetcode234.回文链表

题目传送门&#xff1a;Leetcode234 给你一个单链表的头节点 head &#xff0c;请你判断该链表是否为回文链表。如果是&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 示例 1&#xff1a; 输入&#xff1a;head [1,2,2,1] 输出&#xff1a;true示例 2&…

element中表格组件的row-class-name和class-name属性的使用以及无效处理

1.这两个属性的使用&#xff0c;row-class-name用在el-table标签上&#xff0c;class-name用在el-table-column标签上。两个属性即可绑定类名也可绑定函数 <!-- 这里是绑定函数&#xff0c;也可以绑定类名 --> <el-table :data"tableData" selection-chang…

Ubuntu安装zsh

安装zsh sudo apt install zsh安装git&#xff08;如果有跳过&#xff09; sudo apt install git(1条消息) 关于Git这一篇就够了_17岁boy想当攻城狮的博客-CSDN博客 安装oh my zsh git clone https://github.com/robbyrussell/oh-my-zsh切换目录到oh-my-zsh文件夹下的tools…

VUE--插槽slot(将父级的模块片段插入到子级中)

组件可以接收任意类型的JS值作为props&#xff0c;但我们想要为子组件传递一些模板片段&#xff0c;并在子组件中进行渲染时&#xff0c;此时可以采用插槽slot来实现 简单来说&#xff0c;插槽时组件内留一个或多个插槽的位置&#xff0c;供组件传递对应的模板代码&#xff08;…

ASCII码常用

ASCII码的意义 对程序员来说真的是意义重大啊&#xff01; 作为一名程序员一定要牢记&#xff01; 常用A&#xff1a;65 a&#xff1a;97

设计模式⑦ :简单化

文章目录 一、前言二、Facade 模式1. 介绍2. 应用3. 总结 三、Mediator 模式1. 介绍2. 应用3. 总结 一、前言 有时候不想动脑子&#xff0c;就懒得看源码又不像浪费时间所以会看看书&#xff0c;但是又记不住&#xff0c;所以决定开始写"抄书"系列。本系列大部分内容…

c# 自定义 滑块TrackBar

辛苦半天做出来的&#xff0c;如果觉得好用&#xff0c;记得点赞 效果图如下&#xff1a; 具体操作&#xff1a; 1 、添加代码&#xff08;代码在下面&#xff09;&#xff0c;重新生成下整个工程&#xff0c;在工具栏中就出现控件&#xff0c;将控件拖到窗体中 2、只需要调整…

1434. 数池塘(四方向)-深度优先搜索-DFS

代码&#xff1a; #include<iostream> using namespace std; char a[200][200]; int fx[4]{0,1,0,-1}; int fy[4]{1,0,-1,0}; int k0; int n,m; void dfs(int x,int y){a[x][y].;int tx,ty;for(int i0;i<4;i){txxfx[i];tyyfy[i];if(tx>1&&tx<n&&am…

python24.1.19引入模块

引入方法 可引入第三方模块 安装模块&#xff1a;在终端输入pip install模块名字 eg&#xff1a;

kali下-MSF-ftp_login模块破解FTP账号及密码

一、环境准备 两台设备在同一个网络内 一台kali系统&#xff1a;192.168.10.128 一台winserver2016&#xff1a;192.168.10.132 二、MSF介绍 metasploit 全称是The Metasploit Framework&#xff0c;又称MSF&#xff0c;是Kali 内置的一款渗透测试框架&#xff0c;也是全球…

Docker(二)安装指南:主要介绍在 Linux 、Windows 10 和 macOS 上的安装

作者主页&#xff1a; 正函数的个人主页 文章收录专栏&#xff1a; Docker 欢迎大家点赞 &#x1f44d; 收藏 ⭐ 加关注哦&#xff01; 安装 Docker Docker 分为 stable test 和 nightly 三个更新频道。 官方网站上有各种环境下的 安装指南&#xff0c;这里主要介绍 Docker 在…

【网络技术】【traceroute】【Linux虚拟机(Ubuntu)】无法traceroute至谷歌【及解决方法】

一、问题描述 问题描述如下&#xff1a; Ubuntu虚拟机可以ping通谷歌&#xff08;www.google.com&#xff09;&#xff0c;但是却无法通过traceroute命令找到路由路线&#xff0c;如下图所示&#xff1a; 二、解决方法 从traceroute命令的返回内容可以看出&#xff0c;路由寻…

使用IEEE754标准转换过程

IEEE 754标准是一种用于浮点数表示和计算的标准。对于给定的浮点数&#xff0c;IEEE 754标准定义了如何表示它以及如何执行基本的算术运算。 例如&#xff0c;现在要将十进制浮点数3.25f转换为IEEE 754标准的二进制表示&#xff0c;可以按照以下步骤进行&#xff1a; 将3.25f…

网卡:初始化,frame组装,网卡处理电信号过程

ip模块中存储的是一堆数字信号&#xff0c;网卡内部会把数字信号转换成电信号或者光信号在网线中传输。 网卡只是一个硬件&#xff0c;需要驱动程序去操作他&#xff0c;计算机中已经把主流网卡的驱动程序&#xff08;不只是网卡的&#xff0c;还有鼠标&#xff0c;键盘这些硬件…

性能测试知多少---性能测试流程

看到好多新手&#xff0c;在性能需求模糊的情况下&#xff0c;随便找一个性能测试工具&#xff0c;然后就开始进行性能测试了&#xff0c;在这种情况下得到的性能测试结果很难体现系统真实的能力&#xff0c;或者可能与系统真实的性能相距甚远。 与功能测试相比&#xff0c;性能…

R.swift SwiftGen 资源使用指南

R.swift 和 SwiftGen 资源转换使用指南 R.swift &#xff08;原始代码会打包到项目&#xff1f;&#xff09; Pod platform :ios, 12.0 target LBtest do# Comment the next line if you dont want to use dynamic frameworksuse_frameworks!pod R.swift # pod SwiftGen, ~&g…

虚幻UE 特效-Niagara特效实战-雨天

回顾Niagara特效基础知识&#xff1a;虚幻UE 特效-Niagara特效初识 其他两篇实战&#xff1a;虚幻UE 特效-Niagara特效实战-火焰、烛火、虚幻UE 特效-Niagara特效实战-烟雾、喷泉 本篇笔记我们再来实战雨天&#xff0c;雨天主要用到了特效中的事件。 文章目录 一、雨天1、创建雨…

智汇云舟创始人兼总裁周舟:视频孪生赋能智慧城市多元场景建设

1月18日&#xff0c;由知名科技媒体和产业智库泰伯网主办的WIF2023创新先行者论坛暨企业家会员年会于北京成功举办。共有百余位科技公司创始人、管理者、投资人齐聚&#xff0c;研判产业经济趋势&#xff0c;寻找新形势下企业未来发展的新方向、新机会。智汇云舟创始人兼总裁周…

毅速ESU:金属3D打印技术助力模具开发降本增效

模具是工业之母&#xff0c;在高效批产方面极具优势&#xff0c;但随着企业对模具精度、结构复杂性、生产周期和成本的要求日益严格&#xff0c;传统模具制造已经不能完全满足市场需求。在众多的模具加工工艺新技术中&#xff0c;3D打印技术把增材制造理念引入模具行业&#xf…