【C++】模板进阶
一、非类型模板参数
模板参数分:类型形参与非类型形参。
类型形参:出现在模板参数列表中,跟在class或者typename之类的参数类型名称
非类型形参:就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。
下面size_t N中的N为非类型形参,class T中的T为类型参数。
template<class T, size_t N>
class Stack
{
public://void func()//{// N++;//N是常量不能修改,但是在编译的时候不会出现错误//}
private:int _a[N];int _top;
};
C++20之前,只允许整形做非类型模板参数
C++20,可以支持double(x64)等内置类型
//C++20
template<double X,int* ptr>
class A
{};
注意:
1. 浮点数(float(x86,x64),double(x86))、类对象以及字符串是不允许作为非类型模板参数的。
2. 非类型的模板参数必须在编译期就能确认结果。
这里介绍一个非常鸡肋的头文件
#include<array>
先说这个好处,这个头文件的好处。
看下面图片,如果我们使用静态数组难免会出现越界访问,且编译器也默认没有错误。
如果使用array则会暴力检查出错误
array中使用了非类型模板参数
为什么说array有些鸡肋呢?
因为vector可以完全替代array
下面的代码注意:
template<class T>
void PrintVector(const vector<T>& v)
{// 没有实例化的类模板没实例化时,不能去里面查细节东西。typename vector<T>::const_iterator it = v.begin();while (it != v.end()){cout << *it << " ";it++;}cout << endl;
}
int main()
{vector<int> v1 = { 1, 2, 3 };PrintVector(v1);return 0;
}
这里需要添加typename来明确是类型,因为vector<T>没有实例化,不敢去查const_iterator所以编译器会发生报错
二、模板的特化
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理。这就是模板的特化。
2.1函数模板特化
1. 必须要先有一个基础的函数模板
2. 关键字template后面接一对空的尖括号<>
3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误
template<class T>
bool Less(const T& left, const T& right)
{return left < right;
}
int main()
{cout << Less(1, 2) << endl; Date d1(2022, 6, 7);Date d2(2022, 7, 8);cout << Less(d1, d2) << endl; Date* p1 = &d1;Date* p2 = &d2;cout << Less(p1, p2) << endl; //这里会发生错误,因为是地址的比较而不是对象的比较return 0;
}
所以在函数模板下面进行特化
template<>
bool Less<Date*>( const Date* & left, const Date* & right)
{return *left < *right;
}
我们会发现上面的代码出现了语法不匹配,为什么?
首先,我们先看模板,模板的形参是const T& left,const修饰的是letf,而特化中const修饰的是* left出现了语法不匹配。所以我们要这样修改
template<>
bool Less<Date*>(Date* const & left, Date* const & right)
{return *left < *right;
}
注意:一般函数特化会直接简化形成函数,给这个类型专门匹配一个函数。
2.2类模板特化
类模板特化在实践当中是非常有用的
2.2.1全特化
//类模板
template<class T1, class T2>
class Data
{
public://实现1功能Data() { cout << "Data<T1, T2>" << endl; }
};
//特化
template<>
class Data<int, char>
{
public://实现2功能Data() { cout << "Data<int, char>" << endl; }
};
int main()
{Data<int, int> d1;Data<int, char> d2;return 0;
}
2.2.2偏特化与半特化
//偏特化/半特化
template <class T1>
class Data<T1, int>
{
public:Data() { cout << "Data<T1, int>->偏特化" << endl; }};// 两个参数偏特化为指针类型
template <class T1, class T2>
class Data <T1*, T2*>
{
public:Data() { cout << "Data<T1*, T2*>->偏特化" << endl;cout << sizeof(T1) << endl;cout << sizeof(T2) << endl;cout << typeid(T1).name() << endl;cout << typeid(T2).name() << endl;}
};
// 限定引用
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:Data() { cout << "Data<T1&, T2&>" << endl; }
};
我们不难发现在两个偏特化转化为指针类型,对象d4传的参数为double*,int*。偏特化T1为double,T2为int
三、模板分离编译
首先我们要知道C/C++程序要运行,要经历预处理 ---> 编译 ----> 汇编 ----> 链接
编译:对程序按照语言特性进行词法、语法、语法分析,错误检查无误后生成汇编代码。
注意:头文件不参与编译,但其他.cpp包含的文件中展开。编译器对工程中的多个源文件是分离开单独编译的。
链接:将多个obj文件合并成一个,并处理没有解决的地址问题。
下图,我们不难发现,普通函数在.h文件进行函数声明且.cpp进行定义时不会在链接时找不到地址
若是模板进行分离
Func.cpp中的Add不会编译,生成指令,因为模板没有被实例化。当然也就,没有Add的地址放进符号表,所以链接也就找不到
main.cpp中调用的地方,知道模板实例化成什么,但是只有声明,没有定义。
那么如何进行模板分离?
在Func.cpp中我们进行了模板声明,我们不难发现,我们在声明的代码中把T替换成int与double,使其进行了实例化,但是当main函数中添加Add('x','y');我们又要在Func.cpp进行字符类型的实例化这样十分麻烦。
所以一般把模板定义在.h文件中,如果是模板类,就一般定义在.h中类的外面