前文已经介绍了模板的初阶,介绍了函数模板与类模板,那么这篇文章就针对模板在更近一步,介绍模板进阶内容:非类型模板参数、模板的特化以及模板的分离编译。
非类型模板参数
模板参数可分为类型形参和非类型形参。
类型模板参数:在模板参数列表中 ,是class或typename关键字之后的参数类型名称,也就是我们在初阶文章所用的那类表示。
比如:
template//类型形参
class 类模板名
{
private:
//类内成员声明
};
非类型模板参数: 用一个常数作为类(函数)模板中的一个参数,在类(函数)模板中可将该参数当成常量来使用。
比如:
template
class MyClass {
public:
static const T value = value;
};
这里 T 是类型模板参数,而 value 是非类型模板参数。
当然还可以用其value创建一个静态的数组:
template
class MyClass {
public:
static const T value = value;
private:
T _arr[value];
};
这样设计的代码就可以通过利用非类型模板参数进行定义静态数组。
int main()
{
MyClass s1;//创建了一个大小为存储10个int类型的静态数组
cout << sizeof(s1) << endl;//打印结果:40
MyClass s2;//创建了一个大小为存储10个int类型的静态数组
cout << sizeof(s2) << endl;//打印结果:400
return 0;
}
注意:
1类型限制:非类型模板参数只允许使用整型家族(整型类型,字符类型)类对象以及字符串 是不允许作为非类型模板参数的。
2作用域:非类型模板参数的作用域是模板定义的整个范围。
3类型推断:在使用非类型模板参数时,编译器会根据提供的值推断出参数的类型。
非类型模板参数的优点:
控制固定大小的数组或容器。
根据编译时常量调整算法的行为。
提高性能,通过直接嵌入常量值避免运行时开销。
模板的特化
概念
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理,比如:实现了一个专门用来进行大于比较的函数模板
template
bool Less(T left, T right)
{
return left > right;
}
在我们进行简单的数字比较时
int main()
{
cout << Less(1, 2) << endl; // 可以比较,结果正确
return 0;
}
这样使用是没有问题的,它的判断结果也是我们所预期的,但是我们也可能会这样去使用该函数模板:
int main()
{
int a = 10;
int b = 5;
int p1 = &a, p2 = &b;
cout << Less(p1, p2) << endl; // 不可以比较,结果错误
//实际答案是false,期望结果为true
return 0;
}
判断结果是这两个指针是否构成大于关系,这很好理解,因为我们希望的是该函数能够判断两个指针所指向的内容是否构成大于关系,而该函数实际上判断是确实这两个指针所存储的地址是否构成大于关系,这是两个存在于栈区的指针,所指向的内容不同,其所存储的地址显然是不同的。
类似于上述实例,使用模板可以实现一些与类型无关的代码,但对于一些特殊的类型可能会得到一些错误的结果,此时就需要对模板进行特化,即在原模板的基础上,针对特殊类型进行特殊化的实现方
函数模板特化
依据上面给出的案例,我们得知,当传入的是指针时,我们所期望的是进行比较两者指向的空间存储的信息,而不是比较其二者指针的存储信息,那么此时我们就可以对指针类型进行特殊化的实现,从而达到我们所期望的效果。
//基础的函数模板
template
bool Less(T left, T right)
{
return left > right;
}
//对于指针类型的特化
template
bool Less(T left, T right)
{
return left > right;
}
//对于int类型的特化
bool Less(int left, int right)
{
return left > *right;
}
类模板特化
不仅函数模板可以进行特化,类模板也可以针对特殊类型进行特殊化实现,并且类模板的特化又可分为全特化和偏特化(半特化)。
在介绍类模板的特化之前先介绍一下仿函数
仿函数介绍
include
using namespace std;
class Add {
public:
// 重载函数调用运算符
int operator()(int x, int y) const {
return x + y;
}
};
int main() {
Add add; // 创建 Add 类的对象
cout << "3 + 4 = " << add(3, 4) << endl; // 使用仿函数
return 0;
}
仿函数的优势
状态管理:仿函数可以包含数据成员,因此可以在函数调用之间保持状态。例如,可以创建一个仿函数来计算运行和:
class Accumulate {
private:
int sum;
public:
Accumulate() : sum(0) {}
void operator()(int value) {
sum += value;
}
int getSum() const {
return sum;
}
};
可作为模板参数:仿函数可以作为模板参数传递给 STL 算法,提高代码的灵活性和可重用性。
为什么会在这里介绍仿函数呢?
这是因为仿函数与类模板有着密切的关系:
仿函数与类模板的关系
仿函数可以是类模板:我们可以创建一个类模板来定义仿函数,以便它可以处理不同的数据类型。例如:
template
class Add {
public:
T operator()(T x, T y) const {
return x + y;
}
};
include
include
include
using namespace std;
template
class Compare {
public:
bool operator()(T a, T b) const {
return a > b; // 默认降序
}
};
int main() {
vector vec = {5, 3, 8, 1, 2};
// 使用 Compare 仿函数模板对 vec 进行排序
sort(vec.begin(), vec.end(), Compare());
for (int n : vec)
{
cout << n << " ";
} // 输出: 8 5 3 2 1
return 0;
}
相比之下,类模板其实就是在仿函数的基础上进行修改,使得达到我们想要达到的要求,就比如说全特化:
全特化
举例代码:
//普通的类模板
template
class D
{
public:
D()
{
cout << "D" << endl;
}
private:
T1 a;
T2 b;
};
int main()
{
Dd1;//打印:cout << "D" << endl;
Dd2;//打印:cout << "D" << endl;
return 0;
}
当T1和T2是int,int时,我们若是想对实例化的类进行特殊化处理,那么我们就可以对T1和T2是int和int时的模板进行特化。
首先必须要有一个基础的类模板。
关键字template后面接一对空的尖括号<>。
类名后跟一对尖括号,尖括号中指定需要特化的类型。
生成就会生成一个一个全特化的类模板函数
那么如何证明当T1是int,T2是int时,使用的就是我们自己特化的类模板呢?
当我们实例化一个对象时,编译器会自动调用其默认构造函数,我们若是在构造函数当中打印适当的提示信息,那么当我们实例化对象后,通过观察控制台上打印的结果,即可确定实例化该对象时调用的是不是我们自己特化的类模板了
打印结果:
int main()
{
Dd1;
Dd2;
return 0;
}
偏特化
全特化理解后,偏特化就很好理解了,全特化就是将全部特化,那偏特化不就是特化部分嘛其定义:
//偏特化的类模板
template
class D
{
public:
D()
{
cout << "D" << endl;
}
private:
T1 a;
char b;
};
但是如果这样定义类,那么他走什么呢?
int main()
{
Dd1;
Dd2;
return 0;
}
实际上是:
他的匹配原则就是就近匹配,谁最匹配,就走谁。
补充:
偏特化并不仅仅是指特化部分参数,而是针对模板参数进一步的条件限制所设计出来的一个特化版本。
例如,我们还可以指定当T1和T2为某种类型时,使用我们特殊化的类模板。
//两个参数偏特化为指针类型
template
class D
{
public:
D()
{
cout << "D" << endl;
}
private:
T1 a;
T2 b;
};
//两个参数偏特化为引用类型
template
class D
{
public:
D()
{
cout << "D" << endl;
}
private:
T1 a;
T2 b;
};
int main()
{
Dd1;
Dd2;
Dd3;
return 0;
}
运行结果:
模板的分离编译
什么是分离编译
在分离编译模式下,我们一般创建三个文件,一个头文件用于进行函数声明,一个源文件用于对头文件中声明的函数进行定义,最后一个源文件用于调用头文件当中的函数。
按照此方法,我们若是对一个加法函数模板进行分离编译,其三个文件当中的内容大致如下:
但是使用这三个文件生成可执行文件时,却会在链接阶段产生报错。
下面我们对其进行分析:
我们都知道,程序要运行起来一般要经历以下四个步骤:
预处理: 头文件展开、去注释、宏替换、条件编译等。生成预处理后的代码(main.i)
编译: 检查代码的规范性、是否有语法错误等,确定代码实际要做的工作,在检查无误后,将代码翻译成汇编语言。将源代码(main.i)转换为汇编(main.s),此时并未具体化 Add 函数模板。
汇编: 把编译阶段生成的文件转成目标文件。将汇编代码转换为目标文件(main.o)。
链接: 将生成的各个目标文件进行链接,生成可执行文件。
以上代码在预处理阶段需要进行头文件的包含以及去注释操作。
经过预处理后,就剩下两个文件了一个是Add.i,一个是test.i ,
这两个文件内容如下:
经过预处理后,接下来进入编译阶段。虽然在 main.i 中调用了 Add 函数,但由于 main.i 同时也包含了 Add 函数模板的声明,因此在编译阶段不会发现任何语法错误。编译器顺利将 Add.i 和 main.i 翻译成了汇编语言,并在 Linux 操作系统中生成了 Add.s 和 main.s 文件。
接下来是汇编阶段,编译器利用 Add.s 和 main.s 分别生成了两个目标文件,即 Add.o 和 main.o,这些文件为后续的链接阶段做准备。
到此为止,预处理、编译和汇编过程都顺利完成。但在进行链接操作时,出现了问题:main 函数中调用的 Add 函数并没有得到正确链接。问题的根源在于,Add 函数模板并没有生成实际的函数定义。之所以如此,是因为在整个过程中,函数模板的模板参数 T 并未被实例化,导致编译器无法确定 T 应该对应何种类型。因此,Add 函数模板没有生成具体的函数实现,链接时也就无法找到相应的符号。
模板分离编译失败的原因:
在函数模板定义的地方(Add.cpp)没有进行实例化,而在需要实例化函数的地方(test.cpp)没有模板函数的定义,无法进行实例化。
对应的修改方法就是将Add.c文件修改就可以
//Add.c
//函数模板的定义
template
T Add(const T& x, const T& y)
{
return x + y;
}
template
T Add(const int& x, const int& y);
总结:
带来的优点
减少了代码的重复,从而提高了维护性,
模板在编译时进行类型检查,确保类型安全,避免了类型转换引发的错误。
由于模板是在编译期间实例化的,生成的代码经过优化,通常比运行时多态(如虚函数)更高效。
模板可以用于函数和类,能够处理多种数据类型,提供了高度的灵活性
模板是实现泛型编程的基础,允许算法和数据结构与类型分离,增强了代码的通用性。
缺点:
模板不支持某些类型(如浮点数、类对象、不为非类型参数提供完整类型限制等),可能导致设计上的限制。
对于每种类型的模板实例,编译器会生成独立的代码,这可能导致代码大小增加,尤其是在使用模板的情况下
使用模板可能导致编译时间显著增加,因为模板实例化和类型检查需要消耗额外的时间。