【C++】模板+模板特化

模板

  • 泛型编程
  • 模板
    • 函数模板
      • 定义
      • 原理分析
      • 函数模板的实例化
      • 模板参数的匹配原则
    • 类模板
    • 定义
      • 类模板的实例化
    • 非类型模板参数
  • 模板特化
    • 定义
    • 函数模板特化
    • 类模板特化
    • 应用实例
  • 模板分离编译
    • 原理分析
    • 解决方法
  • 模板总结

铁汁们,今天给大家分享一篇模板+模板特化,来吧,开造⛳️

泛型编程

定义:编写跟具体类型无关的通用代码,是实现代码复用的一种手段。模板是泛型编程的基础。

  • 问:如何实现一个通用的swap函数?
  • 答:写成函数模板,不能在函数重载了。原因:代码复用率低,重载的函数仅是类型不同,若出现了其他新的类型,代码的可维护性降低,若函数中有一处出错了,可能会导致所有的重载函数均出错。

模板

💡模板的本质:本来应该要由我自己写的多份类似代码(除类型不同,代码功能、逻辑相同),现在不需要我自己去写了,我只需要提供一个模板,编译器根据我的实例化,帮我写出代码。

函数模板

定义

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

  • 💡Tips : typename是用来定义模板参数的关键字,也可以使用class,但不能用struct代替class。

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>

using namespace std;

namespace zzx {
    //函数模板
	template<class T>
	void swap(T& left, T& right)
	{
		T& tmp = left;
		left = right;
		right = tmp;
	}
}

int main()
{
	int a = 3;
	int b = 4;

	cout <<"交换前:" << a << ' ' << b << endl;

    zzx:swap(a, b);

	cout << "交换后:" << a << ' ' << b << endl;
	return 0;
}

原理分析

  • 函数模板是蓝图,它本身不是函数,是编译器用特定的方法产生具体类型函数的模具。即:模板其实就是本来应该由我们自己做的重复的事情交给了编译器去做。

  • 编译阶段,编译器通过实参的类型推演生成相对应类型的函数。eg:当用double类型的参数使用函数模板时,编译器根据实参的类型推演出模板参数T的类型,将T确定为double类型,产生一份专门处理double类型的代码。

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>

using namespace std;

namespace zzx {
	template<class T>
	T Add(T& left, T& right)
	{
		return left + right;
	}

	double Add(double& left, double& right)
	{
		return left + right;
	}

	int Add(int& left, int& right)
	{
		return left + right;
	}
}

int main()
{
	int a = 1;
	int b = 2;
	cout << zzx::Add(a, b) << endl;

	double c = 1.1;
	double d = 2.2;
	cout << zzx::Add(c, d) << endl;

	return 0;
}


函数模板的实例化

定义:用不同类型参数使用函数模板时,称为函数模板的实例化。模板参数的实例化有两种,分别为隐式实例化和显示实例化。

  1. 隐式实例化:也称为推演实例化,编译器根据实参的类型,推演出模板参数的实际类型。

  2. 显示实例化:在函数名后加<>来指定模板参数的实际类型。

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>

using namespace std;

namespace zzx {

	template<class T>
	T Add(const T& left, const T& right)
	{
		return left + right;
	}
}

int main()
{
	cout << zzx::Add<int>(1, 2.2) << endl;

	cout << zzx::Add<double>(1, 2.2) << endl;

	return 0;
}

  • 应用场景一:函数参数无模板参数,在函数体内有模板参数 -》只能显示实例化,不能隐式实例化,因为无法通过实参的类型推演出模板参数的实际类型,需要指定模板参数的类型。
template<class T1, class T2>
void fun(int n)
{
	T1 a ;
	T2 b;
}

int main()
{
	fun<int, double>(10);

	return 0;
}
  • 应用场景一:对于stack类,若想让栈1存int类型的数据,让栈2存double类型的数据。
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>

using namespace std;

namespace zzx{
	template<class T>
	class stack {
		T* a;
		size_t top;
		size_t capacity;
	};
}


int main()
{
	zzx::stack<int> s1;

	zzx::stack<double> s2;
	return 0;
}

模板参数的匹配原则

  • 有现成的就吃现成,不要自己动手做 -》非模板函数和同名的模板函数同时存在时,且其他条件相同的情况下,会优先调用非模板函数。
  • 没有现成的,有更合适的就吃更合适的,哪怕要自己动手做 。-》无非模板函数,但与同名的模板函数参数类型匹配,就去调用模板函数。
  • 没有现成的,也没更合适的,就将就吃 -》无非模板函数,与同名的模板函数参数类型不匹配,但普通函数允许隐式类型转化,就去调用普通函数。

    💡Tips : 模板函数不允许隐式类型自动转化,但普通函数支持隐式类型自动转化。

类模板

定义

  • 💡Tips : 类模板后面一定要加分号( ; ) 。

类模板的实例化

类模板只能显示实例化 。类名+<>指定模板参数的实际类型。

  • 显式实例化的类型不同,它们就是不同类。

  • 普通类,类名就是整个类的类型。对于类模板,类名就不是类型,类名+<数据类型>才是整个类的类型。

  • 类模板中的函数要在外面进行定义时,即:声明和定义分离。声明和定义要放在同一文件中,且需要在函数名前加上类名<数据类型>: : 。

非类型模板参数

  1. 模板参数可以分为类型形参和非类型形参。

  2. 类型模板参数:就是普通的类的模板参数,模板参数声明在template 中,T是模板参数。

  3. 非类型模板参数是指在使用模板时传入整形常量,用一个常量作为类(函数)模板的一个参数,该常量只能为整形常量,在编译期间就可以确定其值。用于指定模板的一些具体细节,如数组的长度或函数的输入参数类型。

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<array>

using namespace std;

namespace zzx{
	//非类型模板参数,只能是整形常量
	template<class T, size_t N = 10>
	class array {  

	private:
		T a[N];  //定义一个静态数组
		size_t size;
	};
}


int main()
{
	//array为STL中的一个容器,底层为静态数组,不会初始化,只会在越界的时候进行检查报错
	zzx::array<int, 5> _a1;

	std::array<int, 6> _a2;
	_a2[5];

	return 0;
}

  • array为STL中的一个容器,底层为静态数组,不会初始化,只会在越界的时候进行检查,程序会崩溃。

模板特化

定义

定义:在原模板类(函数)的基础上,针对特殊类型进行特殊化处理的实现方式。模板特化划分为函数模板的特化和类模板的特化。

函数模板特化

1.函数模板特化的步骤

  • 必须先需要一个原函数模板。

  • 关键字template 后+ <>,函数名后+<特化的类型>。

  • 函数的形参表的格式、函数内部的实现逻辑必须要与原函数模板参数格式、内部实现逻辑一致,但如果模板参数的类型为const T&,T为Date*,此时特化后的函数模板参数的类型为T,防止类型转换以及常引用导致结果出错。

#pragma once
#define _CRT_SECURE_NO_WARNINGS 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);
private:
	int _year;
	int _month;
	int _day;
};

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

template<class T>  //函数模板
bool Less (const T& x, const T& y)
{
	return x < y;
}

int main()
{
	Date d1(2022, 7, 8);
	Date d2(2022, 7, 10);

	cout << Less(d1, d2) << endl;  //结果正确

	//按地址比较了,没有按指针指向的内容去比较
	Date* p1 = &d1;
	Date* p2 = &d2;
	cout << Less(p1, p2) << endl;  //结果错误

	Date* p3 = new Date(2022, 7, 8); //连续两次new出来的地址大小无大小关系,new在堆上随意取一块地址
	Date* p4 = new Date(2022, 7, 10);
	cout << Less(p3, p4) << endl;   //结果错误

	return 0;

}

template<class T>  //函数模板
bool Less(T& x, T& y)
{
	return x < y;
}

//template<>  //对函数模板进行特化
//bool Less<Date*>(Date* x, Date* y)
//{
//	return *x < *y;
//}


bool Less(Date* x, Date* y) //普通函数,专门用来处理Date*的比较
{
	return *x < *y;
}
  • Less(p1, p2)结果错误,是因为此处去调用Less函数模板,编译器根据实参的类型推演出T的类型为Date*,在Less内部是按照指针的值(地址)比较的,而不是按照指针指向的内容比较的。
  • 一般不建议对函数模板进行特化,而是直接将该函数实现给出,代码的可读性高。

类模板特化

  1. 全特化:模板参数列表中所有参数的类型全部都是确定的类型。
//全特化
template<>
class AA<char, int>
{
public:
	AA() { cout << "AA<char, int>" << endl; }
private:
	char _d1;
	int _d2;
};


int main()
{
    AA<char, int> a2;

	return 0;
}

  1. 偏特化有两种,一种是特化部分参数,另一种是对参数类型进行条件限制,如:将参数类型偏特化为指针或者是引用类型等。
template<class T1>
class AA<T1, int>
{
public:
	AA() { cout << "AA<T1, int>" << endl; }
private:
	T1 _d1;
	int _d2;
};

int main()
{
    AA<char, int> a2;

	return 0;
}

//偏特化2:对参数类型进行条件限制
template<class T1, class T2> 
class AA<T1*, T2*>  //两个参数偏特化为指针类型
{ 
public:
	AA() { cout << "AA<T1*, T2*>" << endl; }
private:
	T1 _d1;
	int _d2;
};

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

int main()
{ 
    AA<int*, char*> a1;

	AA<int&, char&> a2;

	return 0;
}

  1. 💡匹配顺序:全特化 > 偏特化 > 普通类模板。
//类模板
template<class T1, class T2>
class AA
{
public:
	AA() { cout << "AA<T1, T2>" << endl; }
private:
	T1 _d1;
	T2 _d2;
};

//全特化
template<>
class AA<char, int>
{
public:
	AA() { cout << "AA<char, int>" << endl; }
private:
	char _d1;
	int _d2;
};

//偏特化
template<class T1>
class AA<T1, int>
{
public:
	AA() { cout << "AA<T1, int>" << endl; }
private:
	T1 _d1;
	int _d2;
};

int main()
{
    
	AA<char, int> a1;  //全特化
	 
	AA<int, int> a2;  //偏特化

	AA<int, char> a3;  //普通类模板
    
    return 0;
}

应用实例

  1. 场景一:处理优先队列中T为Date*的数据。
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<queue>

using namespace std;

int main()
{
  
    priority_queue<Date*> pq;
	pq.push(new Date(2022, 7, 8));
	pq.push(new Date(2022, 7, 10));
	pq.push(new Date(2022, 7, 6));

	while (!pq.empty())
	{
		cout << *pq.top() << endl;
		pq.pop();
	}

	return 0;
}

  • 原因分析:因为 priority_queue<class T, class Container = vector, class Compare = less>,此处默认去调用仿函数less,编译器根绝实参的类型推演出T为Date*,导致Less内部是按照指针的值(地址)比较的,而不是按照指针指向的内容比较的。

  • 解决方法:手动写个专门处理Date*类型的仿函数 或者 对仿函数类进行特化。

class less {  //小于
	public:
		bool operator()(const Date* x, const Date* y)
		{
			return *x < *y;
		}
	};
namespace zzx {
	template<class T> 
	class less {  //小于
	public:
		bool operator()(const T& x, const T& y)
		{
			return x < y;
		}
	};

	template<>
	class less<Date*>
	{  //小于
	public:
		bool operator()(Date* x, Date* y)
		{
			return *x < *y;
		}
	};
}


  1. 场景二:处理优先队列中T为任意类型*的数据。采用偏特化的第二种。
namespace zzx {
	template<class T> 
	class less {  //小于
	public:
		bool operator()(const T& x, const T& y)
		{
			return x < y;
		}
	};

	template<class T>  //偏特化2
	class less<T*>
	{  //小于
	public:
		bool operator()(T* x, T* y)
		{
			return *x < *y;
		}
	};
}

int main()
{
	priority_queue<Date*, vector<Date*>, zzx::less<Date*>> pq1;
	pq1.push(new Date(2022, 7, 8));
	pq1.push(new Date(2022, 7, 10));
	pq1.push(new Date(2022, 7, 6));

	while (!pq1.empty())
	{
		cout << *pq1.top() << endl;
		pq1.pop();
	}

	priority_queue<int*, vector<int*>, zzx::less<int*>> pq2;
	pq2.push(new int(1));
	pq2.push(new int(2));
	pq2.push(new int(3));

	while (!pq2.empty())
	{
		cout << *pq2.top() << endl;
		pq2.pop();
	}

	return 0;
}

模板分离编译

原理分析

  1. 分离编译模式:一个项目由若干个文件组成,而每个源文件单独编译生成目标文件,最后将所有的目标文件链接起来形成一个单一的可执行文件的过程称为分离编译模式。

  2. 模板分离编译:模板的声明和定义分离。将模板的声明放在头文件.h文件,把模板的定义放在源文件.cpp文件。

解决方法

  1. 在模板的定义位置处显示实例化,进行类型声明,但这种方法不实用。
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1

using namespace std;

#include"Add.h"

namespace zzx{

	//显示实例化
	template
	int Add<int>(const int&, const int&);

	template
	double Add<double>(const double&, const double&);


	template<class T>
	T Add(const T& left, const T& right)
	{
		return left + right;
	}
}
  1. 将模板的定义和声明放在同一个文件中,将该文件定义为“.hpp”或者“.h”。
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>

using namespace std;

namespace zzx {

	template<class T>
	T Add(const T& left, const T& right);

	template<class T>
	T Add(const T& left, const T& right)
	{
		return left + right;
	}
}

模板总结

  • 优点:模板复用了代码,节省了资源。增强了代码的灵活性。

  • 缺点:会导致代码膨胀,也会导致编译时间变长。不易定位错误(解决方法:排除法,一段一段注释,写一部分就编译一部分。

💡Tips:本质:本来应该由我们自己去写的多份类似代码,我们只需要提供一个模具,让编译器根据实例化帮我们去写。

铁铁们,模板+模板特化就到此结束啦,若博主有不好的地方,请指正,欢迎铁铁们留言,请动动你们的手给作者点个👍鼓励吧,你们的鼓励就是我的动力✨

版权声明:本文为博主作者:奶芙c原创文章,版权归属原作者,如果侵权,请联系我们删除!

原文链接:https://blog.csdn.net/m0_74808907/article/details/136943435

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
乘风的头像乘风管理团队
上一篇 2024年4月1日
下一篇 2024年4月1日

相关推荐