C++——模板(进阶)

1.非类型模板参数

1.1引入

想定义一个静态的栈,该怎么写?

cpp 复制代码
//定义一个静态的栈
#define N 10

template<class T>
class Stack
{
private:
	T _a[N];
};

int main()
{ 
	Stack<int,10> st1;  
	Stack<int,10000> st2;  

	return 0;
}

那如果想让两个栈的空间大小不同,又该怎么做呢?

cpp 复制代码
//定义一个静态的栈
#define N 10000

template<class T>
class Stack
{
private:
	T _a[N];
};

int main()
{ 
	Stack<int,10> st1;     //期望10
	Stack<int,10000> st2;  //期望10000

    //只能把N改为10000,但这太不合适了

	return 0;
}

针对此类问题,C++给了一种更好的解决方式,叫做非类型模板参数

1.2应用

对静态栈这个类进行改造

cpp 复制代码
//之前写的模板参数叫做类型模板参数,比如T
//此时的N就是非类型模板参数
template<class T,size_t N>
class Stack
{
private:
	T _a[N];
};

int main()
{ 
	Stack<int,10> st1;     //期望10
	Stack<int,10000> st2;  //期望10000

	return 0;
}

1.3特点

非类型模板参数还有个特点,它是一个常量

也只有常量才能去定义数组的大小

虽然它是一个常量,但我们可以灵活控制这个整形的大小

我们传一个常量就可以控制,不需要像 #define N 10 一样是写死的,可以在实例化时传

模板参数可以允许去传一个常量

cpp 复制代码
int main()
{ 
	int n;
	cin >> n;
	Stack<int, n> st3;
	//注意:传变量是不行的
	//为什么?
	//编译时要去实例化,如果是变量实例化时是不知道N是多少的,也就无法确定数组开多大


	//注意:目前,非类型模板参数只支持整型,哪怕是浮点型都是不行的
	//char是可以的,因为它也是整型

	return 0;
}

1.4array

在C++11中,出现了一个新容器------array,使用了非类型模板参数

cpp 复制代码
int main()
{
	array<int, 10> a1;
	//它对比的是C语言的数组

	int a2[10];

	cout << sizeof(a1) << endl;
	cout << sizeof(a1) << endl;
	//二者大小相同,都是40,说明二者物理空间大小占用都是一样的

	//它是和对象保持一致的,对象在栈它就在栈,对象在堆就在堆
	//比如对象是new出来的,就是在堆上

	return 0;
}

1.4.1优势

cpp 复制代码
int main()
{
	array<int, 10> a1;
	int a2[10];

	//C语言的数组有个大问题:
	a2[15] = 1;

	//在很多编译器上,比如VS2019,这里不会报错,但实际上是出了问题的
	//因为数组的越界检查是一种抽查,所以没有检查出来

	//这里本质是数组的解引用,会转换成指针的访问去访问第15个位置
	//也没有办法检查,只能去设置标志位
	//VS2022可以在通过语法编译时去强制识别,但场景稍复杂些就不行了

    //array的优势:
	a1[15] = 1;

	//array可以检查出来
	//因为这里本质是一个函数调用,operator()

	return 0;
}

1.4.2实际使用

事实上,array的设计十分鸡肋

cpp 复制代码
int main()
{
	array<int, 10> a1;//不会去初始化
	int a2[10];//不会去初始化

	//C++11的array,设计初衷是期望使用者去替代静态数组
	//但实际上,array设计的十分鸡肋

	vector<int> v(10, 0);//这样写不是更好吗?

	return 0;
}

2.类模板特化

2.1引入

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

int main()
{
    Data<int, int> d1;
//不管传递什么类型,都会去调用上面那个构造函数

    return 0;
}

模板特化:想针对某些类型进行特殊化处理时,就可以使用特化

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


// 模板特化:针对某些类型进行特殊化处理
// 针对它是int和double类型时,进行特殊化处理
template<>
class Data<int,double>
{
public:
    Data() { cout << "Data<int, double>" << endl; }
};
//我们把这个类叫做上面那个类的特化,也就是特殊化

//此时类型是int、douoble会执行下面这个类,不是的话才会去实例化上面那个类

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

    return 0;
}

2.2应用

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


struct PDateCompare
{
	bool operator()(Date* p1, Date* p2)
	{
		return *p1 < *p2;
	}
};


template<class T>
class Less
{
public:
	bool operator()(const T& x, const T& y)
	{
		return x < y;
	}
};

//新的解决方法:
//特化一下,当T是Date*时,进行特殊处理,按指向的对象比较
template<>
class Less<Date*>
{
public:
	bool operator()(Date* x, Date* y)
	{
		return *x < *y;
	}
};


int main()
{
	jxy::priority_queue<Date> q1;
	q1.push(Date(2018, 10, 29));
	q1.push(Date(2018, 10, 28));
	q1.push(Date(2018, 10, 30));
	cout << q1.top() << endl;//2018-10-30

	//jxy::priority_queue<Date*, vector<Date*>, PDateCompare> q2;
	//之前的解决方案是显式传了一个仿函数,使得可以比较日期而不是比较指针
	//q2.push(new Date(2018, 10, 29));
	//q2.push(new Date(2018, 10, 28));
	//q2.push(new Date(2018, 10, 30));
	//cout << *(q2.top()) << endl;

	//那如果不想显式传仿函数,该怎么办呢?
	jxy::priority_queue<Date*> q2;
	q2.push(new Date(2018, 10, 29));
	q2.push(new Date(2018, 10, 28));
	q2.push(new Date(2018, 10, 30));
	cout << *(q2.top()) << endl;

	return 0;

}

详情见上一篇博客:优先级队列

2.3分类

模板特化 分为全特化偏特化

cpp 复制代码
//原模版
template<class T1, class T2>
class Data
{
public:
	Data() { cout << "Data<T1, T2>" << endl; }
private:
	T1 _d1;
	T2 _d2;
};

2.3.1全特化

全特化:即将模板参数列表中所有的参数都确定化。所有的模板参数都特化为一个具体的类型。

cpp 复制代码
//------全特化
template<>
class Data<int, double>
{
public:
	Data() { cout << "Data<int, double>" << endl; }
};

注意: 特化是不能单独存在的,必须要先有原模版特化才可以存在

2.3.2偏特化

偏特化:针对任何模版参数,进行进一步的条件限制,而设计出的特化版本。
偏特化有以下 两种表现方式

2.3.2.1部分特化

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

cpp 复制代码
//------偏特化、半特化:特化部分参数
template<class T1>
class Data<T1, double>
{
public:
	Data() { cout << "Data<T1, double>" << endl; }
};

注意:特化之间可能会有重叠,在匹配时会遵循最匹配原则

cpp 复制代码
int main()
{
	Data<int, int> d1;      //匹配原模版
    Data<int, double> d2;   //匹配全特化,遵循最匹配原则
	Data<double, double> d3;//匹配偏特化

	return 0;
}
2.3.2.2对参数更进一步的限制

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

1.两个参数偏特化为指针类型

cpp 复制代码
//------更特殊、花哨的偏特化
//这里甚至没有指定具体是什么类型
//但是它指示了:如果是指针,就执行该模板
template<class T1, class T2>
class Data<T1*, T2*>
{
public:
	Data() { cout << "Data<T1*, T2*>" << endl; }
};

关于指针的比较,即2.2应用 ,可以进行进一步的改造

cpp 复制代码
//进一步的改造
//此时只要是指针,就会按照指向的对象去比较
template<class T>
class Less<T*>
{
public:
	bool operator()(T* x, T* y)
	{
		return *x < *y;
	}
};

2.两个参数偏特化为引用类型

cpp 复制代码
//两个参数偏特化为引用类型
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:
	Data()
	{
		cout << "Data<T1&, T2&>" << endl;
	}
};


int main()
{
	Data<int&, int&> d5;

	return 0;
}

综上,偏特化的本质 ,其实就是对参数的进一步限制 ,比如限制参数是int类型 ,或是限制参数必须是指针、引用等。

3.函数模板特化

3.1引入

cpp 复制代码
template<class T>
bool Less(T left, T right)
{
	return left < right;
}

int main()
{
	Date* d1 = new Date(2024, 10, 13);
	Date* d2 = new Date(2024, 10, 10);
	cout << Less(d1, d2) << endl;
	//此时答案又是不确定的,时而是1,时而是0

	return 0;
}

3.2应用

cpp 复制代码
//函数模板特化
template<class T>
bool Less(T left, T right)
{
	return left < right;
}

//如果这里是Date*时,想要按照Date去比较
//所以要特化一下
template<>
bool Less<Date*>(Date* left, Date* right)
{
	return *left < *right;
}

//类模板特化,特化在类名的后面
//函数模板特化,特化在函数名的后面

int main()
{
	Date* d1 = new Date(2024, 10, 13);
	Date* d2 = new Date(2024, 10, 10);
	cout << Less(d1, d2) << endl;
	//此时答案就是确定的了

	return 0;
}

3.3实际使用

事实上,类模板建议使用特化,但函数模板是不建议使用特化的

cpp 复制代码
//template<class T>
//bool Less(T left, T right)
//正常情况下,这里不会这么写,传值会有拷贝


template<class T>
bool Less(const T& left, const T& right)//通常是这样写的
{
	return left < right;
}

//想要针对类型是Date*时,进行特殊处理
//就要写特化,一般会把T直接替换成Date*
template<>
bool Less<Date*>(const Date* & left, const Date* & right)
{
	return *left < *right;
}


int main()
{
	Date* d1 = new Date(2024, 10, 13);
	Date* d2 = new Date(2024, 10, 10);
	cout << Less(d1, d2) << endl;

	return 0;
}

3.3.1正确写法

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

int main()
{
	Date* d1 = new Date(2024, 10, 13);
	Date* d2 = new Date(2024, 10, 10);
	cout << Less(d1, d2) << endl;

	return 0;
}

3.4解决方案

为了避免上述这种情况的发生,想对某些类型进行特殊处理时,函数模板可以不使用特化

cpp 复制代码
template<class T>
bool Less(const T& left, const T& right)
{
	return left < right;
}

//写一个具体版本的函数就可以
//利用了编译器的匹配原则
//上面的模板和该函数严格来说不构成重载
//编译器编译时没有上面的模板代码
//模板不实例化,是没有的

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

int main()
{
	Date* d1 = new Date(2024, 10, 13);
	Date* d2 = new Date(2024, 10, 10);
	cout << Less(d1, d2) << endl;

	cout << Less(1, 2) << endl;
	//此时具体的函数才会和模板实例化后的函数构成重载
	//模板会推演、实例化生成一个函数

	return 0;
}

4.关于模板的分离编译

4.1何为分离编译

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

4.2模板不支持分离编译

模板不支持分离编译

而普通函数分离编译,是没有问题的

这是为什么?

4.3原因

这就是模板不支持分离编译的原因。

总结:主要问题还是,Stack.cpp中Add没有实例化。可以认为这就像是一种沟通不畅,

Stack.cpp中有Add的定义,但是它不知道要实例化成什么,所以自然没有函数地址

而Test.cpp中,知道Add的参数要实例化成什么,但是它只有声明,没有定义

4.4解决方案

4.4.1显式实例化

Stack.h文件

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

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

void func();

Stack.cpp文件

cpp 复制代码
#include"Stack.h"

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

void func()
{
	cout << "void func()" << endl;
}

//显式实例化:

//既然定义时不知道要实例化成什么,那就写出来
template
int Add<int>(const int& left, const int& right);
//所以说模板不是不能分离编译,只是需要显式实例化

Test.cpp文件

cpp 复制代码
#include"Stack.h"

int main()
{
	Add(1, 2); 
	func();    

	return 0;
}
4.4.1.1新的问题

Test.cpp文件

cpp 复制代码
#include"Stack.h"

int main()
{
	Add(1, 2); 
	func();    

	//但是这种方法有个很大的缺陷:
	Add(1.1, 2.2);  //call Add<double>(?)

	//此时又会出现链接错误

	return 0;
}

此时就需要在Stack.cpp文件中再写一段实例化的代码

cpp 复制代码
#include"Stack.h"

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

void func()
{
	cout << "void func()" << endl;
}


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

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

所以显式实例化这个方法虽然可行,但是并不好用,

一旦有新的类型去实例化,就需要去显式实例化这个新的类型,否则又会报错

4.4.2关于类模板的分离编译

之前使用的一直是函数模板,接下来我们来看看类模板的分离编译问题

Stack.h文件

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


//类模板
template<class T>
class Stack
{
public:
	void Push(const T& x);
	void Pop();

private:
	T* _a = nullptr;
	int _top = 0;
	int _capacity = 0;
};

Stack.cpp文件

cpp 复制代码
#include"Stack.h"

template<class T>
void Stack<T>::Push(const T& x)
{
	cout << "void Stack<T>::Push(const T& x)" << endl;
}

template<class T>
void Stack<T>::Pop()
{
	cout << "void Stack<T>::Pop()" << endl;
}

//类模板的解决方法可以更加暴力一些
//不需要一个个函数来实例化,可以直接把整个类给实例化
template
class Stack<int>;

但还是同样的问题,一旦有新的类型去实例化,就需要去显式实例化这个新的类型,否则又会报错

所以显式实例化 并不是一个长久之计

Test.cpp文件

cpp 复制代码
#include"Stack.h"

int main()
{
	Stack<int> st;
	st.Push(1);
	st.Pop();

	//与上面的函数模板问题相同
	Stack<double> st1;
	st1.Push(1.1);
	st1.Pop();

	return 0;
}

4.4.3分离在同一个文件中

最合适的解决方案是,不要Stack.cpp文件 ,如果声明和定义要分离,把它们分离在同一个文件中,也就是Stack.h文件

此时可以把Stack.h文件 的名字改为Stack.hpp文件 ,可以更好地说明该头文件中不止有模板的声明 ,还有模板的定义,说明文件中有模板,不方便分离在两个文件中。

Stack.hpp文件

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

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

void func();

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

void func()
{
	cout << "void func()" << endl;
}



//类模板
template<class T>
class Stack
{
public:
	void Push(const T& x);
	void Pop();

private:
	T* _a = nullptr;
	int _top = 0;
	int _capacity = 0;
};

template<class T>
void Stack<T>::Push(const T& x)
{
	cout << "void Stack<T>::Push(const T& x)" << endl;
}

template<class T>
void Stack<T>::Pop()
{
	cout << "void Stack<T>::Pop()" << endl;
}

还有一种解决方法就是,不让声明与定义分离,直接定义。

4.5补充

未来有没有可能去支持分离编译模板?

答案是不会的。模板本身就会增长编译的时间,因为它多了一个实例化的步骤。在编译阶段,都是单对单的处理,每个源文件都会分离、单独编译,也因此Add不会实例化,只有在最后的链接阶段,才会进行合并。

如果支持分离编译的话,就意味着在编译阶段 ,如果看到了有模板的存在,就要去把其它所有的文件都找一遍,寻找哪里在使用这个模板,实例化成什么,才能去支持分离编译,但是这样会大大地增长编译的时间

编译器编译时只会向上查找,就是为了提高编译速度

5.关于模板的总结

5.1优点

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

5.2缺点

  1. 模板会导致代码膨胀问题,也会导致编译时间变长
  2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误
相关推荐
m0_748255262 小时前
前端安全——敏感信息泄露
前端·安全
Yhame.3 小时前
深入理解 Java 中的 ArrayList 和 List:泛型与动态数组
java·开发语言
编程之路,妙趣横生3 小时前
list模拟实现
c++
鑫~阳4 小时前
html + css 淘宝网实战
前端·css·html
Catherinemin4 小时前
CSS|14 z-index
前端·css
mazo_command5 小时前
【MATLAB课设五子棋教程】(附源码)
开发语言·matlab
88号技师5 小时前
2024年12月一区SCI-加权平均优化算法Weighted average algorithm-附Matlab免费代码
人工智能·算法·matlab·优化算法
IT猿手5 小时前
多目标应用(一):多目标麋鹿优化算法(MOEHO)求解10个工程应用,提供完整MATLAB代码
开发语言·人工智能·算法·机器学习·matlab
青春男大5 小时前
java栈--数据结构
java·开发语言·数据结构·学习·eclipse
88号技师5 小时前
几款性能优秀的差分进化算法DE(SaDE、JADE,SHADE,LSHADE、LSHADE_SPACMA、LSHADE_EpSin)-附Matlab免费代码
开发语言·人工智能·算法·matlab·优化算法