【C++】模版进阶

目录


前言

        对模版初阶的一些内容进行补充,有模版的特化和为什么模版不支持分离编译的原因。


一、非类型模版参数

在模版初阶中,我们定义的模版参数一直是以类型为主,但模版参数也可以是非类型的。

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

示例:定义一个静态数组

#include <iostream>
using namespace std;

template<class T, size_t N = 10>
class Array
{
public:
	Array()
		:_size(N)
	{ }

	size_t size()
	{
		return _size;
	}

private:
	T _a[N];
	size_t _size;
};

int main()
{
	Array<int> arr1;
	cout << arr1.size() << endl;

	Array<double, 20> arr2;
	cout << arr2.size() << endl;

	return 0;
}

运行结果:

注意:

  • 浮点数、类对象以及字符串是不允许作为非类型模板参数的。
  • (浮点数在 C++20 中支持作为非类型模版参数,但是类对象始终不支持作为非类型模版参数)
  • 非类型的模板参数必须在编译期就能确认结果。

补充:在C++11中,,新增了个 array 静态数组

  • (不常用,不详细介绍)


二、模版的特化

概念:

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

1.函数模版特化 

示例:

#include <iostream>
using namespace std;

//日期类
class Date
{
public:
	Date(int year = 0, int month =0, int day =0)
		:_year(year)
		,_month(month)
		,_day(day)
	{ }

	bool operator<(const Date& x) const
	{
		if (_year < x._year)
		{
			return true;
		}
		else if (_year == x._year && _month < x._month)
		{
			return true;
		}
		else if (_year == x._year && _month == x._month && _day < x._day)
		{
			return true;
		}

		return false;
	}

private:
	int _year;
	int _month;
	int _day;
};

//小于比较
template<class T>
bool Less(const T& left, const T& right)
{
	return left < right;
}

int main()
{
	int a = 1, b = 2;
	cout << Less(a, b) << endl;//普通类型,可以比较

	Date d1(2025, 1, 1);
	Date d2(2025, 3, 8);
	cout << Less(d1, d2) << endl;//类类型,可以比较

	Date* p1 = new Date(2025, 1, 1);
	Date* p2 = new Date(2025, 3, 8);
	cout << Less(p1, p2) << endl;//指针类型,无法比较

	return 0;
}

运行结果:

  • 可以看出,Less 模版函数可以处理内置类型和类类型的比较,但是无法处理指针类型,因为指针类型需要解引用再去比较,不解引用比较的是地址大小这毫无意义。
  • 这时候我们可以使用函数模版特化解决这个问题。

函数模版特化步骤:

  1. 必须要先有一个基础的函数模板
  2. 关键字template后面接一对空的尖括号<>
  3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
  4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇 怪的错误。

接着上面函数模版进行特化:

//小于比较
template<class T>
bool Less(const T& left, const T& right)
{
	return left < right;
}

//Less特化
template<>
bool Less<Date*>(Date* const& left, Date* const& right)
{
	return *left < *right;
}

运行结果:

注意:

  • Less 的特化函数参数为 Date* const& left,之所以 const 写在 * 的右边,是因为在原模版函数中,参数 const T& left 中,const 实际修饰的是 left 本身,也就是 left 本身不能改变,在指针中,这种修饰是 const 放在 * 后修饰指针本身而不是指向的内容。
  • 所以这一点就是模版函数的缺陷,因此不建议过多使用函数模版的特化。

一般解决这个问题就采用之前学的函数重载即可:

//小于比较
template<class T>
bool Less(const T& left, const T& right)
{
	return left < right;
}

//Less重载
bool Less(const Date*& left, const Date* right)
{
	return *left < *right;
}

小结: 

  • 一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出(重载)。
  • 该种实现简单明了,代码的可读性高,容易书写,因为对于一些参数类型复杂的函数模板,特化 时特别给出,因此函数模板不建议特化。


2.类模版特化

类模版特化分为两种:

  1. 全特化
  2. 偏特化

偏特化有两种表现方式:

  1. 部分特化
  2. 参数更进一步的限制

类模版特化的步骤与函数模版类似


1.全特化

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

示例:

#include <iostream>
using namespace std;

//原模版
template<class T1, class T2>
class Data
{
public:
	Data()
	{
		cout << "Data<class T1, class T2>" << endl;
	}

private:
	T1 _d1;
	T1 _d2;
};

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

private:
	char _d1;
	int _d2;
};

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

	return 0;
}

运行结果:

  • 只要参数是 char 和 int 就会调用到全特化的类模版


2.偏特化:

  • 偏特化:任何针对模版参数进一步进行条件限制设计的特化版本。
(1)部分特化
  • 将模板参数类表中的一部分参数特化。

示例:

#include <iostream>
using namespace std;

//原模版
template<class T1, class T2>
class Data
{
public:
	Data()
	{
		cout << "Data<class T1, class T2>" << endl;
	}

private:
	T1 _d1;
	T1 _d2;
};

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

private:
	char _d1;
	int _d2;
};

//偏特化:部分特化
template<class T1>
class Data<T1, int>//将第二个参数特化为int
{
public:
	Data()
	{
		cout << "Data<class T1, int>" << endl;
	}

private:
	T1 _d1;
	int _d2;
};

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

	return 0;
}

运行结果:

  • 我们可以看到 d1 因为第二个参数为 int 因此调用到了偏特化版本的类模版。d2 即使第二个参数为 int,但是 d2 因为第一个参数为 char 更符合全特化版本,因此 d2 还是调用全特化版本。d3 没有特化版本可匹配因此调用原模版。
  • 只需记住一句话:哪个模版更符合参数要求就调用哪个模版实例化。


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

示例:

//偏特化:参数更进一步的限制
template<class T1,class T2>
class Data<T1*, T2*>
{
public:
	Data()
	{
		cout << "Data<T1*, T2*>" << endl;
	}

private:
	T1* _d1;
	T2* _d2;
};

template<class T1,class 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;
};

int main()
{
	Data<int*, int*> d4;
	Data<int&, int&> d5(1, 2);

	return 0;
}

运行结果:

  • 注意:第二个模版的引用参数必须调用初始化列表进行初始化。
  • 使用进一步限制的偏特化还有一个好处,就是模版参数还可以单独使用,也就是说模版参数类型并不是指针或者引用,这一点可以说使用 typeid 的 name 方法验证:
//偏特化:参数更进一步的限制
template<class T1,class T2>
class Data<T1*, T2*>
{
public:
	Data()
	{
		cout << "Data<T1*, T2*>" << endl;
		cout << typeid(T1).name() << endl;
		cout << typeid(T2).name() << endl;
	}

private:
	T1* _d1;
	T2* _d2;
};

template<class T1,class T2>
class Data<T1&, T2&>
{
public:
	Data(const T1& d1, const T2& d2)
		:_d1(d1)
		,_d2(d2)
	{
		cout << "Data<T1&, T2&>" << endl;
		cout << typeid(T1).name() << endl;
		cout << typeid(T2).name() << endl;
	}

private:
	const T1& _d1;
	const T2& _d2;
};

运行结果:


3.类模版特化应用示例

  • 我们之前学过优先级队列,也就是堆,我们试着将 Date 类对象插入堆。
  • 一个堆插入 Date 类对象,另一个堆插入 Date 类对象的指针,然后出堆排序。
  • 因为优先级队列默认是大堆,因此排序应是从大到大。
  • 最后观察结果:
#include <iostream>
#include <queue>
using namespace std;

//日期类
class Date
{
	//友元
	friend ostream& operator<<(ostream& out, const Date& x);
public:
	Date(int year = 0, int month =0, int day =0)
		:_year(year)
		,_month(month)
		,_day(day)
	{ }

	bool operator<(const Date& x) const
	{
		if (_year < x._year)
		{
			return true;
		}
		else if (_year == x._year && _month < x._month)
		{
			return true;
		}
		else if (_year == x._year && _month == x._month && _day < x._day)
		{
			return true;
		}

		return false;
	}

private:
	int _year;
	int _month;
	int _day;
};

ostream& operator<<(ostream& out, const Date& x)
{
	out << x._year << "年" << x._month << "月" << x._day << "日" << endl;
	return out;
}

int main()
{
	Date d1(2025, 1, 1);
	Date d2(2025, 3, 8);
	Date d3(2025, 2, 2);

	priority_queue<Date> pq1;
	pq1.push(d1);
	pq1.push(d2);
	pq1.push(d3);
	while (!pq1.empty())
	{
		cout << pq1.top();
		pq1.pop();
	}
	cout << endl;

	priority_queue<Date*> pq2;
	pq2.push(&d1);
	pq2.push(&d2);
	pq2.push(&d3);
	while (!pq2.empty())
	{
		cout << *pq2.top();
		pq2.pop();
	}
	cout << endl;

	return 0;
}

运行结果:

  • 结果很明显,传 Date 对象是正常排序,而指针是按地址排序的。
  • 现在问题是如何让第二个堆也是排序日期而不是地址。
  • 这时候就可以利用类模版的特化,将仿函数 less 特化一个专门针对 Date* 的版本。

加入以下代码:

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

观察最后结果:

  • 问题得到解决。
  • 现在说下原理,我们之前学过优先级队列,它的模版参数大致为:template<class T, class Container = vector<T>, class Compare = less<T>>,后面两个模版参数我们没有特殊要求就不传,所以我们只传了 Date* 给 T,这时候仿函数 less 就会示例化一份 Date* 的类,但是因为我们写了一个特化版本的 less,因此调用的是特化版本的less类进行实例化,最后得到正确结果。


三、模版分离编译

  • 你之前是不是很疑惑为什么定义类模版时,成员函数的声明和定义不能分离到两个文件?
  • 我们在C语言阶段,想要模拟实现一个容器时,一般是准备两个文件,一个 .h 文件专门存放函数声明,一个 .c 文件专门存放函数的定义,这样方便管理。可是在 C++ 中,当我们学会模版后,用模版模拟实现一个容器时却不支持将声明和定义分离。

如下,一个模版函数,如果声明和定义分离,会出现的报错:

a.h 文件

#pragma once

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

a.cpp 文件

#include "a.h"

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

test.cpp

#include <iostream>
#include "a.h"
using namespace std;

int main()
{
	cout << Add(1, 2) << endl;

	return 0;
}

报错信息:

  • 报错信息是以 LNK 开头,表示这是一个链接错误,一般情况下,链接错误出现在只有声明没有定义的情况。

了解编译过程:(详细过程主页有)

  • 首先,我们得知道,代码想变为可执行程序,就需要经过编译和链接的过程,编译又分为预处理、编译、汇编3个过程。
  • 预处理 --> 编译 --> 汇编 --> 链接。
  • 预处理:展开头文件,处理条件编译指令、删除注释等,最后生成 .i 文件
  • 编译:编译过程就是将预处理后的文件进行一系列的:词法分析、语法分析、语义分析及优化,生成相应的 .s 汇编代码文件
  • 汇编:汇编代码转化成二进制指令
  • 链接:把一堆文件链接在⼀起才生成可执行程序。

 原因解释:

  • 既然是链接错误,那么问题就出现在链接里面,我们知道普通函数声明和定义可以分离,普通函数地址在编译时确认,链接时候通过地址调用对应函数。
  • 而模版只所以报了链接错误,就是因为模版没有实例化,而编译没有报错的原因是因为有声明存在,声明相当于承诺,声明会给出地址,只要定义存在,最后就可以通过地址跳转几次找到函数。但是模版函数定义在另一个文件,模版函数没有实例化就不能使用,模版函数想实例化就需要模版参数,但是模版参数在声明哪里,为什么模版参数信息传递不到另一个文件中?
  • 因为每个文件都是单线做栈的,也就是说每个文件都是单独编译,最后再通过链接连在一起。编译期间,函数模版没有实例化,因为没有模版参数。没有实例化就没有具体函数的地址,所以最后报错。

 简单点说:

  •  由于函数模版的声明和定义分离,导致编译时,函数模版的声明中有参数信息但是缺少函数定义无法实例化,函数模版定义文件光有定义但没有参数信息无法实例化。最后链接时找不到具体函数导致报错。

疑惑: 

  • 你可能想问为什么编译时不将参数信息传递给定义文件?
  • 这就涉及文件编译的效率问题,如果在一个工程目录下,文件的数量非常大,即使在单线做栈的情况下,也是就每个文件单独编译最后链接的情况下,应该工程目录在一起编译都需要半个小时甚至几个小时(根据文件数量大小)。当然项目工程一般会模块化完成,最后在一起编译。所以,你想想,如果在编译期间每个文件还要向其他文件传递模版参数信息,哪编译的效率将会直线下降。

解决方法:

为了不影响编译的效率,保持每个文件单独编译,以下有两种解决方法:

  1. 在定义文件中,手动写好需实例化的函数。
  2. 模版函数的声明和定义不要分离。这样就能在编译时自动实例化。

当然想都不用想,选择第二种,第一种完全抛弃了模版的优势。


四、模版总结

模版优点:

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

模版缺点:

  1. 模板会导致代码膨胀问题,也会导致编译时间变长。
  2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误

总结

        以上就是本文的全部内容,感谢支持!