模版进阶
1. 非类型模版参数
模板参数可分为类型形参和非类型形参。
- 类型形参: 出现在模板参数列表中,跟在
class
或typename
关键字之后的参数类型名称。- 非类型形参: 用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。
示例:实现一个静态数组的类
template<class T, size_t N> //N:非类型模板参数
class StaticArray
{
public:
size_t arraysize()
{
return N;
}
private:
T _array[N]; //利用非类型模板参数指定静态数组的大小
};
这样就可以在实例化不同的类的对象的时候,通过非类型模版参数指定类中的参数当做常量来使用。
但是这样的方法和C语言中的宏定义的方式十分类似,但是宏定义有一个缺陷,因为 N
的大小是固定的,所以只能定义出 N
个元素大小的 arr对象,那么此时如果想要同时定义两个大小为 10 和 100 的数组显然是做不到的。
注意事项:
- 非类型模板参数也可以给定缺省值。
- 非类型模板参数只适用于整形,不适用于浮点数、类对象以及字符串。
- 非类型的模板参数必须在编译期间就能确认结果,即非类型模板参数的实参只能是常量。
2. 模板的特化
2.1 模板特化的概念
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理,比如,实现了一个专门用来进行小于比较的函数模板:
// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
return left < right;
}
void test4()
{
cout << Less(1, 2) << endl; // 可以比较,结果正确
Date d1(2022, 7, 7);
Date d2(2022, 7, 8);
cout << Less(d1, d2) << endl; // 可以比较,结果正确
Date* p1 = &d1;
Date* p2 = &d2;
cout << Less(p1, p2) << endl; // 可以比较,结果错误
}
运行结果:
1
1
0
012FF9CC
012FF9C0
可以看到,Less
绝对多数情况下都可以正常比较,但是在特殊场景下就得到错误的结果;上述示例中,p1
指向的 d1
显然小于 p2
指向的d2
对象,但是 Less
内部并没有比较 p1
和 p2
指向的对象内容,而比较的是 p1
和 p2
指针的地址,这就无法达到预期而发生错误。
此时,就需要对模板进行特化 – 即在原模板类的基础上,针对特殊类型进行特殊化的处理;模板特化中分为函数模板特化与类模板特化。
2.2 函数模版特化
函数特化步骤:
- 必须要先有一个基础的函数模板。
- 关键字
template
后面接一对空的尖括号<>
。- 函数名后跟一对尖括号,尖括号中指定需要特化的类型。
- 函数形参表必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
函数模版特化示例:
// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
return left < right;
}
//对Date*类型进行模板特化
template<>
bool Less<Date*>(Date* left, Date* right)
{
return *left < *right; //特殊处理:比较left和right指向的内容,而不是left和right本身
}
int main()
{
cout << Less(1, 2) << endl; //非Date*类型,走普通模板实例化
Date d1(2025, 6, 10);
Date d2(2025, 7, 8);
cout << Less(d1, d2) << endl; //非Date*类型,走普通模板实例化
Date* p1 = &d1;
Date* p2 = &d2;
cout << Less(p1, p2) << endl; //Date*类型,走特化的函数模版
return 0;
}
但是这里如果想实现,当参数为两个指针的时候调用 Less 实现其他功能的话可以使用函数重载,可以将重载一个/多个特殊类型的形参。
**注意:**一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出 (函数重载)。
函数重载的示例:
// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
return left < right;
}
//Date*类型的函数重载
bool Less(Date* left, Date* right)
{
return *left < *right; //特殊处理:比较left和right指向的内容,而不是left和right本身
}
int main()
{
cout << Less(1, 2) << endl; //非Date*类型,走普通模板实例化
Date d1(2025, 6, 10);
Date d2(2025, 7, 8);
cout << Less(d1, d2) << endl; //非Date*类型,走普通模板实例化
Date* p1 = &d1;
Date* p2 = &d2;
cout << Less(p1, p2) << endl; //Date*类型,走参数类型为Date*的重载函数
return 0;
}
如上,对于一些参数类型复杂的函数模板直接给出,即实现为函数重载,这种方法该种实现简单明了,代码的可读性高,容易书写,因此函数模板不建议特化。
2.3 类模版特化
类模板特化又分为全特化与偏特化。
2.3.1 全特化
全特化即是将模板参数列表中所有的参数都确定化,如下:
//创建Date类
template<class T1, class T2>
class Data
{
public:
Data() { cout << "Data<T1, T2>" << endl; }
private:
T1 _d1;
T2 _d2;
};
//类模板全特化
template<>
class Data<int, char>
{
public:
Data() { cout << "Data<int, char>" << endl; }
private:
int _d1;
char _d2;
};
int main()
{
Data<int, int> d1;
Data<int, char> d2;
}
运行结果:
Data<T1, T2>
Data<int, char>
2.3.2 偏特化
2.3.2.1 部分特化
将模板参数类表中的一部分参数特化。
//创建Date类
template<class T1, class T2>
class Data {
public:
Data() { cout << "Data<T1, T2>" << endl; }
private:
T1 _d1;
T2 _d2;
};
//部分特化--将第二个参数特化为int
template <class T1>
class Data<T1, int>
{
public:
Data() { cout << "Data<T1, int>" << endl; }
private:
T1 _d1;
int _d2;
};
int main()
{
Data<int, char> d1; //使用普通模板实例化
Data<int, int> d2; //第二个参数与模板特化中的特化参数相同,优先使用特化模板进行实例化
}
在程序中可以将模板中的部分参数显示指定为某种具体类型,这样模板参数在进行匹配时会优先匹配。
2.3.2.2 参数进行进一步限制
偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一 个特化版本。
//创建Date类
template<class T1, class T2>
class Data {
public:
Data() { cout << "Data<T1, T2>" << endl; }
private:
T1 _d1;
T2 _d2;
};
//参数更进一步限制--两个参数偏特化为指针类型
template <typename T1, typename T2>
class Data <T1*, T2*>
{
public:
Data() { cout << "Data<T1*, T2*>" << endl; }
private:
T1 _d1;
T2 _d2;
};
//参数更进一步限制--两个参数偏特化为引用类型
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:
Data(const T1& d1, const T2& d2)
: _d1(d1)
, _d2(d2)
{
cout << "Data<T1&, T2&>" << endl;
}
private:
const T1& _d1;
const T2& _d2;
};
int main()
{
Data<int, double> d2; // 调用基础的模板
Data<int*, int*> d3; // 调用特化的指针版本
Data<int&, int&> d4(1, 2); // 调用特化的指针版本
}
如上代码,可以通过偏特化对模板参数进行进一步的限制,比如模板参数定义为 <T*, T*>
,这样只要实参为指针类型,不管是 int*
、double*
、还是 vector<int>*
,都会调用此特化模板。又比如将模板参数定义为为 <T&, T&>
,这样只要实参是引用类型就会调用此特化模板。从而实现了在限制参数类型的同时又不会将参数局限为某一种具体类型。
3. 模版的分离编译
3.1 分离编译的概念
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。
3.2 分离编译的详解
在分离编译模式下,一般创建三个文件,一个头文件用于进行函数声明,一个源文件用于对头文件中声明的函数进行定义,最后一个源文件用于调用头文件当中的函数。
按照此方法,若是对一个加法函数模板进行分离编译,其三个文件当中的内容大致如下:
但是使用这三个文件生成可执行文件时,却会在链接阶段产生报错。
具体分析:
一段程序如果需要运行起来需要经历下面四个阶段:
- 预处理: 头文件展开、去注释、宏替换、条件编译等。
- 编译: 检查代码的规范性、是否有语法错误等,确定代码实际要做的工作,在检查无误后,将代码翻译成汇编语言。
- 汇编: 把编译阶段生成的文件转成目标文件。
- 链接: 将生成的各个目标文件进行链接,生成可执行文件。
以上代码在预处理阶段需要进行头文件的包含以及去注释操作。
**预处理:**这三个文件经过预处理后实际上就只有两个文件了,若是对应到Linux操作系统当中,此时就生成了 Add.i 和 main.i 文件了。
编译:预处理后就需要进行编译,虽然在 main.i 当中有调用Add函数的代码,但是在 main.i 里面也有Add函数模板的声明,并且C++的编译器允许有声明的函数通过编译,因此在编译阶段并不会发现任何语法错误,之后便顺利将 Add.i 和 main.i 翻译成了汇编语言,对应到Linux操作系统当中就生成了 Add.s 和 main.s 文件。
**汇编:**之后就到达了汇编阶段,此阶段利用 Add.s 和 main.s 这两个文件分别生成了两个目标文件,对应到Linux操作系统当中就是生成了 Add.o 和 main.o 两个目标文件。
链接:前面的预处理、编译和汇编都没有问题,现在就需要将生成的两个目标文件进行链接操作了,但在链接时发现,在main函数当中调用的两个Add函数实际上并没有被真正定义,主要原因是函数模板并没有生成对应的函数,因为在全过程中都没有实例化过函数模板的模板参数T,所以函数模板根本就不知道该实例化T为何类型的函数。
模板分离编译失败的原因:
在函数模板定义的文件中(Add.cpp)因为不知道要实例化模版成什么类型就没有实例化;而在需要实例化函数的地方(main.cpp)虽然知道要实例化成 int
和 double
各一份 Add 函数,但是之后函数声明没有模板函数的定义,无法进行实例化。
解决方法
- 模板定义的位置进行显示实例化模板定义的位置进行显示实例化
在函数模板定义的地方,对 T
为 int
和 double
类型的函数进行了显示实例化,这样在链接时就不会找不到对应函数的定义了,也就能正确执行代码了。
- 模版声明和定义都放在一个文件中
虽然第一种方法能够解决模板分离编译失败的问题,但是并不推荐这种方法,因为需要用到一个新的类型函数模板实例化的函数,就需要自己手动显示实例化一个函数,非常麻烦。
所以推荐第二种方法,模板来说最好不要进行分离编译,不论是函数模板还是类模板,将模板的声明和定义都放到一个文件当中就行了。
4. 模版总结
优点:
- 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生。
- 增强了代码的灵活性。
缺陷:
- 模板会导致代码膨胀问题,也会导致编译时间变长。
- 出现模板编译错误时,错误信息非常凌乱,不易定位错误。