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. 出现模板编译错误时,错误信息非常凌乱,不易定位错误
相关推荐
雾喔1 分钟前
算法练习5
算法·代理模式
名字还没想好☜4 分钟前
React 受控输入框光标跳到末尾:格式化输入时的 selection 丢失 bug 与修复
前端·javascript·react.js·bug·react·next.js
Access开发易登软件5 分钟前
Access 怎么做前后端分离?用 Web API 读写 SQL Server
前端·数据库·人工智能·microsoft·excel·access
10share14 分钟前
React 新一代样式隔离方案 —— 编译时、零运行时、原生写法
前端·react.js
问商十三载16 分钟前
RAG 检索召回越多越好?2026 信噪比模型深度解析
人工智能·算法·机器学习
额恩6617 分钟前
ResearchPilot 第三阶段总结报告
python·算法
Revolution6124 分钟前
Node.js 是什么:前端项目里哪些事情由它完成
前端·面试·node.js
Hi李耶29 分钟前
【LeetCode】9-回文数
算法·leetcode·职场和发展
红叶舞30 分钟前
基于线段树的数据结构
数据结构·python·算法
Richard.Wong36 分钟前
qt生成dll供C#调用
开发语言·qt