C++模板:非类型模板参数,模板特化(函数模板与类模板,全特化与偏特化),模板的分离编译(模板为什么不推荐分离与定义分离)
目录
前言
在现代 C++ 中,模板(Template)是一项极其强大的特性。
它使得我们可以编写与类型无关的代码,从而实现高复用性、高灵活性和高性能的泛型编程。
本篇文章将系统讲解以下三个模板进阶知识点:
-
非类型模板参数
-
模板特化
- 函数模板特化
- 类模板特化(含偏特化)
-
模板的分离编译
一、非类型模板参数
1.1 概念介绍
在模板参数中,除了常见的类型参数(typename / class)外,还可以使用常量值作为模板参数,这种参数称为 非类型模板参数。
template<class T, size_t N>
class Array {
public:
T& operator[](size_t index) { return _array[index]; }
const T& operator[](size_t index) const { return _array[index]; }
size_t size() const { return N; }
private:
T _array[N];
};
这里的 N 就是一个非类型模板参数,它在编译期间必须是可确定的常量。
1.2 使用注意事项
| 限制项 | 说明 |
|---|---|
| 必须是编译期常量 | 不能是运行时变量 |
| 不允许的类型 | 浮点数、类对象、字符串常量 |
| 常见类型 | 整型、枚举、指针、引用等 |
示例:
Array<int, 10> arr; //正确,10 是编译期常量
Array<double, 3.14> a; //错误,浮点数不能作为非类型模板参数
1.3 易错概念辨析
提问: 下面代码中的s1和s2是同一个类型吗?
template<class T = int, size_t N = 100>
class Stack
{
private:
T _a[N];
};
int main()
{
Stack<int, 10> s1;
Stack<int, 1000> s2;
return 0;
}
不是同一个类型!
二者只是使用了同一个模板,在编译过程中会实例化为两个不同的类
在vs2022中,软件识别s1的类型为Stack<int, 10U>,s2的类型为Stack<int, 1000U>。
说明虽然我们只是往模板里传入两个不同的值,但是编译器会根据传入的值,将模板实例化为不同的类型!
简而言之,是让编译器完成了本该我们手写的工作。
二、模板特化
2.1 为什么需要特化?
模板的设计初衷是通用性,但有些类型的行为特殊,需要额外处理。
例如指针类型的比较操作,如果直接使用 <,比较的是地址而不是内容,这时就需要“特化”模板。
模板特化就是针对特定类型重新定义模板的实现方式。
2.2 函数模板特化
以下是一个普通的比较函数模板:
template<class T>
bool Less(T left, T right) {
return left < right;
}
对于普通类型(如 int、double)没问题;
但若传入指针类型,则比较的是内存地址!
template<class T>
bool Less(T left, T right)
{
return left < right;
}
int main()
{
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; // 可以比较,结果错误
return 0;
}
此处比较的是开辟的内存空间的地址的大小,因此每次运行结果都是随机的,因为开辟的地址不一定按顺序开辟。
解决办法:函数模板特化
编译器在选择使用哪个方法时,会优先选择最合适的进行使用(类似于运算符重载时选择匹配的参数列表),如果没有实现对应的类型,那么就会走函数模板进行实例化。然而,如果有已经实现好的模板函数(也就是特化好的模板函数),那么编译器会优先使用实现好的,类似下面的代码:传入Date*类型的值时,会直接使用这个特化好的函数
template<>
bool Less<Date*>(Date* left, Date* right) {
return *left < *right; // 比较指针指向的对象
}
使用示例:
Date d1(2022, 7, 7), d2(2022, 7, 8);
Date* p1 = &d1;
Date* p2 = &d2;
cout << Less(p1, p2); // 调用特化版本
小提示:
对于复杂函数模板,函数模板特化编写成本高。
如果某个类型确实特殊,直接写一个普通函数往往更简洁。
2.3 类模板特化
类模板特化主要分为:
| 类型 | 描述 |
|---|---|
| 全特化(Full Specialization) | 所有模板参数都被确定 |
| 偏特化(Partial Specialization) | 只固定部分参数或对参数模式进行限定 |
(1)全特化
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;
};
测试结果:
Data<int, int> d1; // 调用通用模板
Data<int, char> d2; // 调用全特化模板
输出:
Data<T1, T2>
Data<int, char>
(2)偏特化
偏特化是指只“部分特化”模板参数,或限定某种类型模式。
示例1:对第二个模板参数特化为 int
template<class T1, class T2>
class Data {
public:
Data() { cout << "Data<T1, T2>" << endl; }
};
template<class T1>
class Data<T1, int> {
public:
Data() { cout << "Data<T1, int>" << endl; }
};
示例2:特化为指针类型
template<typename T1, typename T2>
class Data<T1*, T2*> {
public:
Data() { cout << "Data<T1*, T2*>" << endl; }
};
示例3:特化为引用类型
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;
};
调用结果表:
| 调用语句 | 实际调用版本 |
|---|---|
Data<int, double> d1; |
基础模板 |
Data<double, int> d2; |
偏特化(T2 = int) |
Data<int*, int*> d3; |
偏特化(指针版) |
Data<int&, int&> d4(1,2); |
偏特化(引用版) |
2.4 类模板特化应用案例
在 STL 中,std::less 是一个常见的比较器类。下面模拟其简化实现:
template<class T>
struct Less {
bool operator()(const T& x, const T& y) const {
return x < y;
}
};
但是,当我们用 Less<Date*> 比较日期指针时,会比较地址。
这时就可以使用类模板特化:
template<>
struct Less<Date*> {
bool operator()(Date* x, Date* y) const {
return *x < *y;
}
};
这样,只要Date类里面支持<的运算符重载,就可以正确比较日期类的大小
结果:
| 比较对象 | 结果 |
|---|---|
Less<Date>()(d1, d2) |
比较对象内容 |
Less<Date*>()(p1, p2) |
比较指针指向的对象 |
三、模板的分离编译
3.1 普通分离编译
C++ 通常将声明放在 .h 文件,定义放在 .cpp 文件,最后链接生成可执行文件。
但模板不同,编译器需要在实例化时看到完整定义。
3.2 问题示例
// a.h
template<class T>
T Add(const T& left, const T& right);
// a.cpp
template<class T>
T Add(const T& left, const T& right) {
return left + right;
}
// main.cpp
#include "a.h"
int main() {
Add(1, 2);
Add(1.0, 2.0);
}
上面代码会报错:
编译器无法在实例化模板时找到定义。
3.3 解决方案
| 方法 | 说明 | 是否推荐 |
|---|---|---|
| 将声明与定义放在同一头文件中 | 通常使用 .hpp 或直接放 .h |
推荐 |
| 显式实例化(Explicit Instantiation) | 在 .cpp 中显式声明具体类型 |
不推荐 |
显式实例化示例:
// a.cpp
template int Add<int>(const int&, const int&);
template double Add<double>(const double&, const double&);
但这种方式灵活性差,实际项目中一般使用第一种方法。
以下是个人学习时的源代码,没有经过系统整理,谨慎观看!
不过这些源码确实体现了我的思考思路,没有文章内容那么偏向理论,因此我选择贴出来,但是没有作为独立章节展示,大家可以自由选择是否查看~
没有什么高深的内容,完全可以忽略
// Func.h
#pragma once
template<class T>
void FuncT(const T& x)
{
cout << "void FuncT(const T& x)" << endl;
}
void FuncF();
// Func.cpp
#include<iostream>
using namespace std;
#include"Func.h"
template<class T>
void FuncT(const T& x)
{
cout << "void FuncT(const T& x)" << endl;
}
void FuncF()
{
cout << "void FuncF()" << endl;
}
// Test.cpp
#include<iostream>
using namespace std;
#include"Func.h"
void test1()
{
FuncT(1);
FuncT(1.1);
FuncF();
Stack<int> st;
st.Push(1);
}
/*
* 编译 -》 检查语法,生成汇编代码
* 一个函数调用汇编是
* call 函数地址
* 虽然这里还没得到准确的函数地址,但是可以通过编译检查
* 而真正的函数地址在链接的时候进行检查
*
* 汇编 -》 生成二进制机器码
* Func.o Test.o
*
* 链接 -》 合并链接,生成可执行程序
* a.out
* (此处链接了各个cpp文件,因此上面的call得到了函数地址)
*
* 那么为什么模板函数分离与定义分离会发生链接错误?
* 说明链接时没填上地址(前面过程都一样)
*
* 对于普通函数而言,它会被编译为指令
* 但是模板函数在没有实例化时,不知道里面的真实类型,无法被编译为指令
* (虽然在test.cpp中实例化了,但是各个.cpp文件在链接之前是不会有交互的)
*/
/*
* 改回声明与定义在同一个文件中后,
* include"Func.h"相当于在这里拷贝了头文件里的内容,
* 此时 FuncT 就不需要链接了,因为本cpp文件就有定义,
* 编译的时候直接就完成了实例化,生成了汇编代码,
* 汇编时直接就获得了汇编指令,获得了地址
*
* 相当于哪里包含了头文件,哪里就有定义,
* 哪里有定义,哪里就不需要链接,
* 调用的对方就有定义,不存在链接问题
*
* 链接不上的原因是func.cpp那边没有实例化模板,链接找不到地址
*/
/*
* 显示实例化太麻烦,实践中没人用
* 使用模板的地方直接包含了定义,
* 编译时实例化生成了函数的地址,不需要模板
*/
int main()
{
test1();
return 0;
}
四、模板优缺点总结
| 优点 | 缺点 |
|---|---|
| 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生 | 模板会导致代码膨胀问题,也会导致编译时间变长 |
| 增强了代码的灵活性 | 出现模板编译错误时,错误信息非常凌乱,不易定位错误 |
总结
C++ 模板让我们实现了 “类型参数化编程”,是泛型思想的核心。
掌握非类型参数、特化与分离编译,不仅能写出高效的模板代码,还能更深入理解 STL 的设计原理。
💬 学习建议:
- 尝试自己实现一个简化版的
vector或Less比较器。- 使用偏特化练习不同类型匹配模式。
- 研究 STL 源码中
std::allocator与std::iterator_traits的模板实现。
更多推荐



所有评论(0)