【C++】16:模板进阶

目录

一、前言

二、什么是C++模板

三、非类型模板参数

四、Array容器

五、模板的特化

[5.1 概念](#5.1 概念)

[5.2 函数模板特化](#5.2 函数模板特化)

[5.3 类模板特化](#5.3 类模板特化)

[5.3.1 全特化](#5.3.1 全特化)

[5.3.2 偏特化](#5.3.2 偏特化)

[5.3.3 类模板特化应用示例](#5.3.3 类模板特化应用示例)

六、模板分离编译

[6.1 什么是分离编译](#6.1 什么是分离编译)

[6.2 模板的分离编译](#6.2 模板的分离编译)

[6.3 解决方法](#6.3 解决方法)

七、总结


一、前言

在我们学习C++时,常会用到函数重载。而函数重载,通常会需要我们编写较为重复的代码,这就显得臃肿,且效率低下。重载的函数仅仅只是类型不同,代码的复用率比较低,只要有新类型出现时,就需要增加对应的函数。此外,代码的可维护性比较低,一个出错可能会导致所有的重载均出错。

那么,模板的出现,就让这些问题有了解决方案,在之前的文章中已经详细的讲解了C++的 ----- 模板初阶,所以本次博客将为大家详细的讲解C++的模板进阶

二、什么是C++模板

在学习泛型编程之前,我们先要来写一个交换int类型的函数,如下所示:

cpp 复制代码
void Swap(int& x, int& y) {
    int tmp = x;
    x = y;
    y = tmp;
}

这个函数只能交换int类型的数据,如果我们想要交换double和char类型的数据呢,就需要使用函数重载来在写两个Swap函数,如下所示:

cpp 复制代码
void Swap(int& x, int& y) {
    int tmp = x;
    x = y;
    y = tmp;
}
void Swap(double& x, double& y) {
    double tmp = x;
    x = y;
    y = tmp;
}
void Swap(char& x, char& y) {
    char tmp = x;
    x = y;
    y = tmp;
}

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

  • 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数
  • 代码的可维护性比较低,一个出错可能所有的重载均出错

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

C++引入了模板的概念,就像在模具中浇筑一样,我们只需要浇筑不同的材料液,就能到由对应材料的物体。C++中,也能够存在这样一个模具,通过给这个模具中填充不同材料(数据类型),来获得不同材料的铸件(即生产具体类型的代码)。

泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。

模板又分为函数模板和类模板。

更加具体 模板初阶知识 大家这一去看看之前的文章 ----- 模板初阶

这篇文章主要讲解 模板的高阶 操作:非类型模板参数、全特化、偏特化等

三、非类型模板参数

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

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

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

假如我们现在有一个类Stack,这个类里面有一个静态数组来模拟栈,我想要定义一个Stack的一个对象类存储10个数据,想要定义一个Stack对象存放20个数据,就可以实现非类型模板参数,如下所示:

cpp 复制代码
#include<iostream>
using namespace std;

template<size_t N=5>
class Stack
{
private:
	int _a[N];
	int _top;
};
int main()
{
	Stack<10> st1;//可以存放10个数据
	Stack<15> st2;//可以存放20个数据
	Stack<> st3;//可以存放5个数据
	return 0;
}

底层上,编辑器还是生成了三个类,一个类N是10,一个类N是20,一个类N是5。

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

3.非类型模板参数主要是整型常量,如int、short、bool、char、long、long long

四、Array容器

库里面的Array容器就使用了非类型模板参数,C++11以后才支持的。如下所示:

T表示array里面存放的数据类型,N表示能存多少个数据。

也支持迭代器和[]等操作,它的迭代器也是一个原生指针,如下所示:

但是array不支持push等操作,因为array不支持扩容。

array的底层是一个静态数组。使用非类型模板参数会更加灵活,如下所示:

cpp 复制代码
#include<iostream>
using namespace std;
#include<array>

int main()
{
	array<int, 10> a1;
	array<double, 100> a2;
	return 0;
}

array相比与数组:

主要就是越界检查的问题

数组:越界读不检查,越界写抽查

array:越界读写都可以检查
array相比于vector:

array实现的功能vector都可以实现,我们在实际中很少用array,因为vector比array好太多了,不仅array的功能vector都有,vector还能动态的扩容。

还有就是array的数据是存放到栈上的,vector上的数据是存放在堆里面的。

五、模板的特化

5.1 概念

通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些

错误的结果,需要特殊处理,比如:实现了一个专门用来进行小于比较的函数模板

cpp 复制代码
#include<iostream>
using namespace std;
class Date
{
	friend ostream& operator<<(ostream& _cout, const Date& d);
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);
	}

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(T left, T right)
{
	return left < right;
}
int main()
{
	cout << Less(1, 2) << endl; // 可以比较,结果正确
	Date d1(2022, 7, 7);
	Date d2(2022, 7, 3);
	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指针的地址,这就无法达到预期而错误。

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

5.2 函数模板特化

函数模板的特化步骤:

1.必须要先有一个基础的函数模板

2.关键字template后面接一对空的尖括号>

3.函数名后跟一对尖括号,尖括号中指定需要特化的类型

4.函数形参表:必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。

我们就可以将上面的Less模板特化,让T是Date*类型的,如下所示:

cpp 复制代码
#include<iostream>
using namespace std;
class Date
{
	friend ostream& operator<<(ostream& _cout, const Date& d);
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);
	}

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(T left, T right)
{
	return left < right;
}
//特化
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, 3);
	cout << Less(d1, d2) << endl; // 可以比较,结果正确
	Date* p1 = &d1;
	Date* p2 = &d2;
	cout << Less(p1, p2) << endl; // 可以比较,结果错误
	return 0;
}

运行结果如下:

当我们传入的参数是Date*类型的时候,就会执行下面的特化版本。

注意:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。
bool Less ( Date * left , Date * right )
{
return * left < * right ;
}

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

注意:这里有一个坑需要大家注意,

一般函数的参数是自定义类型的时候,我们都会加引用来减少拷贝构造,如果不改变参数就需要加const,同时特化的模板也需要更改,如下所示:

cpp 复制代码
// 函数模板 -- 参数匹配
template<class T>
bool Less(const T& left, const T& right)
{
	return left < right;
}
//特化
template<>
bool Less<Date*>(const Date* & left, const Date* & right)
{
	return *left < *right;
}

修改代码之后再运行会报错,如下所示:

这是为什么呢?

这是因为:

第一个函数的const修饰的是left,left不允许改变,

第二个函数的const修饰的是*left,既left指向的内容不允许修改

所以需要将const放到*后面去,如下所示:

cpp 复制代码
// 函数模板 -- 参数匹配
template<class T>
bool Less(const T& left, const T& right)
{
	return left < right;
}
//特化
template<>
bool Less<Date*>( Date* const& left,  Date* const& right)
{
	return *left < *right;
}

5.3 类模板特化

5.3.1 全特化

全特化即是将模板参数列表中所有的参数都确定化。如下所示:

cpp 复制代码
#include<iostream>
using namespace std;

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;
};
int main()
{
	Data<int, char> d1;
	Data<int, int> d2;

	return 0;
}

运行如下所示:

d2匹配的原模版,d1匹配的是全特换版本。

只要第一个参数是int类型,第二个参数是char类型,就会匹配全特化版本的模板。

5.3.2 偏特化

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

cpp 复制代码
#include<iostream>
using namespace std;

template<class T1,class T2>
class Data
{
public:
	Data()
	{
		cout << "Data<T1, T2>" << endl;
	}
private:
	T1 _d1;
	T2 _d2;
};
//全特化
template<>
class Data<int,char>//第一个参数是int,第二个参数是char就匹配我
{
public:
	Data()
	{
		cout << "Data<int, char>" << endl;
	}
private:
	int _d1;
	char _d2;
};
//偏特化
template<class T1>
class Data<T1, double>//第二个参数只要是double类型就匹配我
{
public:
	Data()
	{
		cout << "Data<T1, double>" << endl;
	}
private:
	T1 _d1;
	double _d2;
};
int main()
{
	Data<int, char> d1;
	Data<int, int> d2;
	Data<int, double> d3;
	Data<char, double> d4;
	return 0;
}

运行如下所示:

d3和d4的第二个参数是double类型的,所以就会匹配偏特化版本,只要第二个参数是double类型的,第一个参数无论是什么类型,就会匹配这个偏特化版本的模板。

注意,如果定义一个自定义类型的对象,既能匹配全特化版本,又能匹配偏特化版本,这种情况会匹配全特化版本,如下所示:

cpp 复制代码
#include<iostream>
using namespace std;

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;
};
//偏特化
template<class T1>
class Data<T1, char>
{
public:
	Data()
	{
		cout << "Data<T1, char>" << endl;
	}
private:
	T1 _d1;
	char _d2;
};
int main()
{
	Data<int, char> d1;
	Data<double, char> d2;
	return 0;
}

此时d1的第一个参数是int类型,第二个参数是char类型,既能匹配全特化版本,又能匹配偏特化版本,这种情况就会走全特化版本,

d2的第一个参数是double,第二个参数是char,只能匹配偏特化版本。

运行如下所示;


注意:也能特化指针和引用类型,如下所示:

cpp 复制代码
#include<iostream>
using namespace std;

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;
};
//偏特化
template<class T1>
class Data<T1, char>
{
public:
	Data()
	{
		cout << "Data<T1, char>" << endl;
	}
private:
	T1 _d1;
	char _d2;
};
//两个参数偏特化为指针类型
template <typename T1, typename T2>
class Data <T1*, T2*>
{
public:
	Data() { cout << "Data<T1*, T2*>" << endl; }
};
//两个参数偏特化为引用类型
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:
	Data()
	{
		cout << "Data<T1&, T2&>" << endl;
	}
};
//一个参数是指针类型,一个是引用类型
template <typename T1, typename T2>
class Data <T1*, T2&>
{
public:
	Data() { cout << "Data<T1*, T2&>" << endl; }
};
int main()
{
	Data<int, char> d1;
	Data<double, char> d2;
	Data<int*, char*> d3;
	Data<double*, char*> d4;
	Data<int*, double*> d5;
	Data<double&, char&> d6;
	Data<int&, double&> d7;
	Data<double*, char&> d8;
	
	return 0;
}

只要两个参数都是指针类型,就会匹配指针类型的特化;

只要两个参数都是引用类型,就会匹配引用类型的特化。

运行如下所示:

5.3.3 类模板特化应用示例

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

cpp 复制代码
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
class Date
	{
	friend ostream& operator<<(ostream& _cout, const Date& d);
	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);
		}
	
	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>
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>());
	for (auto& ele : v1)
	{
		cout << ele << 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& ele : v2)
	{
		cout << *ele << endl;
	}
	return 0;
}

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

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

六、模板分离编译

6.1 什么是分离编译

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

6.2 模板的分离编译

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

cpp 复制代码
// 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;
}

分析:

6.3 解决方法

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

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

七、总结

模板的优点

  • 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
  • 增强了代码的灵活性

模板的缺点

  • 模板会导致代码膨胀问题,也会导致编译时间变长
  • 出现模板编译错误时,错误信息非常凌乱,不易定位错误
相关推荐
AndrewHZ22 分钟前
【图像处理基石】如何使用大模型进行图像处理工作?
图像处理·人工智能·深度学习·算法·llm·stablediffusion·可控性
AndrewHZ25 分钟前
【图像处理基石】图像处理的基础理论体系介绍
图像处理·人工智能·算法·计算机视觉·cv·理论体系
CoderIsArt1 小时前
SAM-5 核心类体系的 C++ 完整设计
c++·sam5
CS_浮鱼1 小时前
【Linux进阶】mmap实战:文件映射、进程通信与LRU缓存
linux·运维·c++·缓存
YJlio1 小时前
「C++ 40 周年」:从“野蛮生长的指针地狱”到 AI 时代的系统底座
c++·人工智能·oracle
纵有疾風起2 小时前
C++——多态
开发语言·c++·经验分享·面试·开源
稚辉君.MCA_P8_Java2 小时前
Gemini永久会员 Java实现的暴力递归版本
java·数据结构·算法
冯诺依曼的锦鲤2 小时前
算法练习:差分
c++·学习·算法
有意义3 小时前
栈数据结构全解析:从实现原理到 LeetCode 实战
javascript·算法·编程语言