C++基础(22)——模板的进阶
目录
非类型模板参数
模板的特化
相关概念
函数模板的特化
类模板的特化
全特化
偏特化
1、部分特化
2、参数的进一步限制
模板的分离编译
什么是模板的分离编译
模板的分离编译
解决方法
模板总结
非类型模板参数
我们的模板参数可以被分为类型形参以及非类型形参。
类型形参:出现在我们的模板的参数列表里面,一般跟在我们的class或是typename之后的参数类型名称。
非类型形参:就是用一个常量来作为我们类(函数)模板的一个参数,在类(函数)模板中可以当成常量来使用。
比如下面的这个代码,就用到了类型和非类型形参:
namespace xywl {template<class T, size_t N = 10> class array {public:size_t size() const {return N;}private:T _array[N];};
}
有了这上面的非类型的模板参数之后,我们就可以在示例化的时候就指定我们的数组的大小了:
int main() {xywl::array<int, 20> a;cout << a.size() << endl;return 0;
}
敲黑板:
1、我们这里的非类型模板参数的类型不能是浮点数、类的对象和字符串,只能是整数那一类。
2、非类型模板参数是在编译阶段就确认了的。
模板的特化
相关概念
我们可以先来看一个具体的栗子来理解为什么要模板特化,比如我们想要实习一个比较的函数,来判断一个数是不是小于一个数。
代码如下:
// 函数模板
template <class T>
bool Less(T left, T right) {return left < right;
}
我们的调用示例如下:
int main() {cout << Less(1, 2) << endl; // 可以比较且结果正确Date d1(2025, 9, 25);Date d2(2025, 10, 1);cout << Less(d1, d2) << endl; // 可以比较且结果正确Date* p1 = &d1;Date* p2 = &d2;cout << Less(p1, p2) << endl; // 可以比较但结果错误return 0;
}
这里我们可以看到,我们实现的Less函数在绝大多数的情况下面都是可以正常执行比较逻辑的,但是在我们的一些特殊的场景下面就会得到错误的结果,上述的示例中我们比较的并不是我们想要的能容而是指向了两个对象的地址,这就是我们无法预期的错误了。
这个时候我们就要堆模板进行特化处理了,也就是在原来的模板类的基础上面针对我们的特殊类型进行特殊化的实现,模板的特化实际上分为了函数模板的特化和类模板的特化。
函数模板的特化
我们上面的栗子中我们传入的是指针,我们要的是进行对象之间的判断,所以我们这里需要对于Date*类型进行特殊化的处理才可以实现。
函数模板特化的步骤:
1、我们首先要有一个基础的函数模板,在这个栗子中就是我们上面的这个函数模板。
2、关键字templete后面加上一个空的尖括号。
3、函数名后面跟上一对尖括号,里面写上我们要特化的类型。
4、函数形参表中必须要和我们的模板函数的基础参数类型相同,如果不同就会报出一些奇怪的错误,这里我们的基础参数类型就是我们的Date*了。
实现代码如下:
template <class T>
bool Less(T left, T right) {return left < right;
}
template<>
bool Less<Date*>(Date* left, Date* right) {return *left < *right;
}
int main() {cout << Less(1, 2) << endl; // 可以比较且结果正确Date d1(2025, 9, 25);Date d2(2025, 10, 1);cout << Less(d1, d2) << endl; // 可以比较且结果正确Date* p1 = &d1;Date* p2 = &d2;cout << Less(p1, p2) << endl; // 可以比较但结果错误return 0;
}
敲黑板:
其实我们这里这么些就是多此一举了,因为我们这个时候通常是将这个需要特化的函数直接给出。代码如下:
bool Less(Date* left, Date* right) {return *left < *right;
}
类模板的特化
全特化
全特化就是将我们的类模板的参数列表中的所有的参数都确定下来。
类模板特化的步骤如下:
1、首先我们要有一个基础的类模板。
2、关键字后面也是要跟一对空的尖括号。
3、类名后面跟上一对尖括号,尖括号的里面就是我们指定要特化的类型。
例如下面的类模板:
template<class T1, class T2>
class Date {public:Date() {cout << "Date<T1, T2>" << endl;}private:T1 _d1;T1 _d2;
};
// 对于我们的T1和T2分别是int和char时候的模板进行特化处理
template<>
class Date<int, char>
{public:Date() {cout << "Date<int, char>" << endl;}private:int _d1;char _d2;
};
偏特化
偏特化就是对于我们的模板参数进行进一步条件限制的特化版本,比如对于我们下面给出的这一个模板类:
template <class T1, class T2>
class Date {public:Date() {cout << "Date<T1, T2>" << endl;}private:T1 _d1;T2 _d2;
};
偏特化有两种形式的特化方式:
1、部分特化
也就是对于我们的模板参数的部分参数进行了特化。
例如下面的栗子:
template <class T1>
class Date<T1, int>
{public:Date() {cout << "Date<T1, int>" << endl;}private:T1 _d1;int _d2;
};
这个时候我们只要实例化对象的时候指定T2是int,就会使用这个特化的类模板来实例化对象了。
测试效果:
2、参数的进一步限制
其实我们的偏特化不仅仅只是特化部分的参数,还可以对我们的模板参数更一步的条件限制搜设计出一个全新的特化版本。
示例如下:
template <class T1, class T2>
class Date<T1&, T2&>
{public:Date() {cout << "Date<T1&, T2&>" << endl;}private:T1 _d1;T2 _d2;
};
测试效果:
模板的分离编译
什么是模板的分离编译
一个程序可以是由我们的多个源文件共同实现的,而每一个源文件单独编译生成目标文件,最后将我们的所有目标文件链接起来形成单一的可以执行的文件的过程我们称之为分离编译。
模板的分离编译
我们在分离编译的情况下,一般会自己创建出单个文件,一个是用来做函数的声明,另一个是用来对于头文件声明的函数进行定义的,最后一个是调用头文件的源文件。
但是我们如果在实现模板的时候也这么写就会报错的,下面就是我们的具体的分析:
我们先来补充回忆一下我们的C/C++程序运行的具体流程(4个步骤):
1、预处理:头文件被展开,去除注释,宏被替换等等等等。
2、编译:检查代码的规范性,是否含有语法的错误,确定代码实际要做的工作,检查没有问题后将我们的代码变成会汇编语言。
3、汇编:把编译生成的文件转成目标文件
4、链接:将多个目标文件进行链接生成可执行文件。
我们这里来举个栗子:
三个文件:
将这几个文件预处理了之后,得到了两文件分别是Add.i和mani.i:
我们预处理之后就要进行编译的操作了,这个阶段没有什么错误,我们可以顺利地将我们的两个.i文件给翻译成汇编语言,对应的就是生成了Add.s和main.s文件了。之后我们在汇编阶段就可以生成我们的Add.o和main.o文件了,前面的处理都没有什么问题,但是到了链接的时候,我们在main函数调用Add函数的时候发现函数并没被定义,因为这个过程都没有实例化过我们的函数模板的模板参数T,所以我们的函数模板就不知道我们的T为什么类型的函数了。
敲黑板:
我们在函数模板定义的时候没有实例化(Add.cpp),在需要实例化函数的地方没有模板函数的定义,也就无法进行实例化了。
解决方法
我们这里有两种解决方法:
第一种:将声明和定义放到一个文件里面(xxx.hpp)或是放在xxx.h,推荐。
第二种:在模板定义的地方显示实例化,不推荐。
模板总结
优点
1、模板复用了代码,节约了资源,C++标准模板库(STL)因此而来。
2、增强了代码的灵活性。
缺点
1、模板会导致我们的代码膨胀,也会导致我们的编译时间增长。
2、出现了模板编译错误的时候,错误信息会非常凌乱,不容易定位。