【C++】模板进阶 —— 非类型模板参数 | 特化 | 模板的分离编译

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

@TOC 正文开始

1. 非类型模板参数

模板参数分为类型形参与非类型形参。类型模板参数,即出现在模板参数列表中,跟在class或typename后的参数类型名称。

有如下场景,我要实现一个静态栈,别管 ——

#define N 100 //宏推荐用const替代

template<class T>
class Mystack
{
public:
    //...
private:
	T _a[N];
	size_t _top;
};

但是,这样无法灵活控制栈的大小 ——

	Mystack<int> st1; //100
	Mystack<int> st1; //200

:purple_heart: 这就要引入非类型模板参数

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

template<class T,size_t N = 10>
class Mystack
{
public:
	//...
private:
	T _a[N];
	size_t _top;
};

int main()
{
	Mystack<int,100> st1; //100
	Mystack<int,200> st2; //200
	Mystack<int> st3; //可以给缺省参数:从右向左确,且连续
	return 0;
}

注意:

  • 非类型模板参数必须是整型常量(整形家族:char, short, int, long long),不能是浮点数、字符串、自定义类型。
  • 可以给缺省参数:从右向左缺,且连续

2. 模板的特化

2.1 函数模板的特化

:yellow_heart: 函数模板的特化格式 ——

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

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

  3. 在函数名和参数之间,写<指定需要特化的类型>

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

template<class T>
bool objLess(T left,T right)
{
	return left < right;
}

//// 参数匹配:有现成的,不去匹配模板
//bool objLess(Date* left, Date* right)
//{
//	//可以写年月日比较,也可以用类对象的函数重载operator<
//	return *left < *right; 
//}

// 函数模板特化:专用化
template<>
bool objLess<Date*>(Date* left, Date* right)
{
	return *left < *right;
}

int main()
{
	cout << objLess(1, 2) << endl; //一般情况下,可以直接比较

	Date* p1 = new Date(2022, 4, 13);
	Date* p2 = new Date(2022, 4, 19);
    // 不想用Date*指针比较,想按日期比较
	cout << objLess(p1, p2) << endl;

	return 0;
}

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

附:依然使用了我们的老朋友日期类Date

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

2.2 类模板的特化

特化,即特殊化,比如需要针对某些类型做特殊化处理

2.2.1 全特化

全特化,是将模板参数列表中所有参数确定化

<img src="C:\Users\13136\AppData\Roaming\Typora\typora-user-images\image-20220413173643459.png" alt="image-20220413173643459" style="zoom:80%;" />

在priority_queue的模拟实现中讲过,如果优先级队列中数据T类型为Date*,默认是按照地址比较的,那这样直接比较的结果不是我想要的,我想要按对象比较。

	void test_priority_queue3()
	{
		//priority_queue<Date*> pq; //默认比较地址大小
		priority_queue<Date*, vector<Date*>, lessPDate> pq;
		pq.push(new Date(2022, 3, 26));
		pq.push(new Date(2022, 4, 1));
		pq.push(new Date(2022, 2, 19));
		pq.push(new Date(2022, 4, 10));

		//默认比较地址大小,若想比较日期大小,自己写仿函数
		while (!pq.empty())
		{
			cout << *pq.top() << endl;
			pq.pop();
		}
	}

在上一篇文章中,我们借助模板的第三个参数Compare的口子,自己写一个仿函数lessPDate传过去。现在再提供一种方式,针对Date*特化 less ——

	template<class T>
	struct less
	{ 
		// 大堆
		bool operator()(const T& x, const T& y)
		{
			return x < y;
		}
	};

	template<>
	struct less<Date*>
	{
		// 大堆
		bool operator()Date*& x, Date*& y)
		{
			// of course可以自己写按年月日比较,也可以调用Date类重载的operator<
			return *x < *y; 
		}
	};

这样,不需要显式的传仿函数,实际上是对Date*类型的比较方式进行了特化 ——

	priority_queue<Date*> pq;

2.2.2 偏特化

偏特化,是对模板参数进一步进行条件限制的。

比如下面模板类 ——

using namespace std;

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

偏特化有两种表现方式 ——

:purple_heart: 部分特化

将模板参数类表中的一部分参数特化。如下代码中,第一个参数随意,第二个参数是指定值 ——

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

int main()
{
	Data<int, int> d1; //模板
	Data<int, char> d2; //上一页全特化

	// 偏特化
	Data<char, char> d3;
	Data<double, char> d4;

	return 0;
}

:purple_heart: 参数更进一步的限制

如下面一段代码,是指针类型,但不管你是什么类型的指针 ——

//两个参数偏特化为指针类型
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;
};

int main()
{
	Data<char*, char*> d5;
	Data<double*, char*> d6;

	Data<int&, int&> d7(1,2);
	return 0;
}

注:非类型模板参数也可以特化 ——

#include<iostream>

using namespace std;

template<size_t N>
class A
{
public:
	A() { cout << "A<N>" << endl; }
};

template<>
class A<10>
{
public:
	A() { cout << "A<10>" << endl; }
};

int main()
{
	A<100> a1;
	A<10> a2;
	return 0;
}

运行结果 ——

<img src="C:\Users\13136\AppData\Roaming\Typora\typora-user-images\image-20220413211009337.png" alt="image-20220413211009337" style="zoom:80%;" />

3. 模板的分离编译

分离编译:一个程序(项目)由若干个源文件共同实现,每个源文件单独编译形成目标文件,多个目标文件和链接库进行链接处理形成可执行程序,称为分离编译模式。分离编译利于我们维护项目,看.h了解框架设计功能,看.cpp了解实现细节。

:purple_heart: 回顾编译 链接过程

a.h  a.cpp  test.cpp

:small_orange_diamond:1. 预处理:头文件展开 + 条件编译 + 去注释 →

a.i  test.i

:small_orange_diamond:2. 编译:检查语法问题,如果没有问题,就生成汇编代码 →

a.s  test.s

:small_orange_diamond:3. 汇编:把汇编代码转换为二进制代码

a.o  test.o

:small_orange_diamond: 链接:将多个obj文件合并成一个,并处理没有解决的地址问题。

普通函数分离编译没有问题,模板函数分离编译会出现链接不上错误,分析如下 ——

<img src="C:\Users\13136\AppData\Roaming\Typora\typora-user-images\image-20220414102440017.png" alt="image-20220414102440017" style="zoom:100%;" />

:yellow_heart: 解决方法

:small_orange_diamond: 1. 显式实例化

#include"a.h"

void F1(int N)
{
	// 2.编译阶段:生成汇编代码
	cout << "void F1(int N)" << endl;
}


template<class T>
void F2(const T& N)
{
	// 2.编译阶段:不处理,没有实例化,无法生成汇编代码
	cout << "void F2(const T& N)" << endl;
}

// 显式实例化
template
void F2<int>(const int& N);

缺点:用一个就得显示实例化一个,非常麻烦,泛型失去意义。

:small_orange_diamond: 2. 将声明和定义放到一个.h.hpp文件中

那么,在编译阶段,test.i中,头文件展开后,直接就有模板的定义和实例化,可以直接填上调用地址,不需要链接时去找了。

<img src="C:\Users\13136\AppData\Roaming\Typora\typora-user-images\image-20220414105214388.png" alt="image-20220414105214388" style="zoom:80%;" />

4. 模板总结

:purple_heart: 模板优点

  • 模板本质是一种复用,可以更快的迭代开发,C++的标准类模板库(STL)因此而产生。
  • 增强了代码的灵活性。比如仿函数控制比较方式

:purple_heart: 模板缺点

  • 模板会导致代码膨胀问题,实例化不同类型的多份,使可执行程序变大,导致编译时间变长。
  • 出现模板编译错误时,错误信息非常凌乱,不易定位错误。

持续更新~@边通书

猜你喜欢

转载自juejin.im/post/7124855775846989832