目录
读 STL、Boost 这类库时,满眼都是 template,不搞懂泛化、特化和实例化,很容易卡在源码门口。这篇文章咱们把三件事捋清楚:它们各是啥、编译器怎么选版本、顺带用一个”类型是否相同”的小例子串起来。
一、泛化:先有一张"万能图纸"
泛化模板就是最普通的那种模板:用类型参数(或非类型参数)占个位,表示”以后再说啥类型”,代码先按通用逻辑写。
1. 函数模板
一组函数逻辑一样,只是操作的类型不同,就用函数模板兜住。
// T 是占位:具体啥类型,调用时再定
template <typename T>
void swap(T& a, T& b)
{
T temp = a;
a = b;
b = temp;
}
- T 可以是 int、double、std::string 等,都行。
- 注意:模板本身不直接变成机器码,它只是给编译器的一张”生成代码的图纸”。
2. 类模板
类里用到的类型也想抽出来,就用类模板。下面这个迷你容器把数据成员收成 _data,命名上更贴近常见写法。
template <typename T>
class Container {
private:
T _data;
public:
explicit Container(T d) : _data(d) {}
T get() const { return _data; }
};
- 成员函数可以全写在类里;要拆到类外,记得前面带上 template
那一套。
二、特化:图纸不够用,再开一张"专款"
泛化版本对某些类型不合适,或者你想针对性优化,就给这些类型单独写一版,这就是特化。分全特化和偏特化两种。
1. 全特化:参数全钉死
所有模板参数都换成具体类型,把泛化那一套整段换掉。
函数模板全特化示例:对 const char* 单独打招呼。
template <typename T>
void print(T value)
{
std::cout << "Generic: " << value << std::endl;
}
template <>
void print<const char*>(const char* value)
{
std::cout << "Specialized for string: " << value << std::endl;
}
传 “hello” 这种会落到 const char*,优先走特化;别的类型照旧走泛化。
类模板全特化示例:给 int 单独一版 Printer。
template <typename T>
class Printer {
public:
void print(T value)
{
std::cout << "Generic class: " << value << std::endl;
}
};
template <>
class Printer<int> {
public:
void print(int value)
{
std::cout << "Specialized class for int: " << value << std::endl;
}
};
2. 偏特化:只钉死一部分(或改成指针形态)
多参数模板里,可以只固定其中几个参数,或者专门给”两个都是指针”这种形态写一版。函数模板不能做偏特化,只有类模板可以,这点别记反了。
template <typename T, typename U>
class Pair {
public:
Pair(T a, U b) : _first(a), _second(b) {}
void show()
{
std::cout << "Generic: " << _first << ", " << _second << std::endl;
}
private:
T _first;
U _second;
};
// 偏特化:第一个参数固定成 int
template <typename U>
class Pair<int, U> {
public:
Pair(int a, U b) : _first(a), _second(b) {}
void show()
{
std::cout << "Partial (T=int): " << _first << ", " << _second << std::endl;
}
private:
int _first;
U _second;
};
// 偏特化:两个都是指针
template <typename T, typename U>
class Pair<T*, U*> {
public:
Pair(T* a, U* b) : _first(a), _second(b) {}
void show()
{
std::cout << "Partial (pointers): " << *_first << ", " << *_second << std::endl;
}
private:
T* _first;
U* _second;
};
三、实例化:图纸终于变成真代码
实例化就是编译器拿着模板(和可能的特化),按你用的具体类型生成出一份真正的函数或类。模板自己不长肉,实例化之后才落地。
1. 隐式实例化:你写调用,编译器生成
template <typename T>
T add(T a, T b) { return a + b; }
int main()
{
int x = add(1, 2); // 生成 add<int>
double y = add(1.5, 2.5); // 生成 add<double>
return 0;
}
int 一版、double 一版,都是编译器悄悄帮你实例化出来的。
2. 显式实例化:你点名要哪一版
自己写 template …,等于告诉编译器:“这一版在这个翻译单元里给我生成好。“分离编译、控制符号时常用。
template int add<int>(int, int);
template double add<double>(double, double);
template class Container<int>;
实务上很多人把显式实例化放在 .cpp 里,和声明分开,省得链接时重复定义闹心。
四、匹配优先级:编译器先找谁?
可以粗略记一句:特化比泛化”更具体”就优先,全特化、偏特化都比光秃秃的泛化更贴类型;具体规则以标准为准,写库时记住”越特化越像定制款”就行。
三者关系可以一句话说完:泛化是通用图纸,特化是某几类类型的专款,实例化是”按图纸把代码印出来”的那一步。
| 场景 | 适合干啥 |
|---|---|
| 泛化 | 写通用逻辑,例如容器、算法骨架 |
| 特化 | 某类型要特殊行为或要省空间、提速 |
| 显式实例化 | 管住在哪个 .cpp 里生成,配合分离编译 |
五、小例子:用特化做"类型是否相同"
<type_traits> 里那一堆 trait,很多就是泛化 + 偏特化堆出来的。SFINAE(替换失败不是错误)再配合一下,能玩出很多静态判断。这里只演示最朴素的一种:判断两个类型是否相同。
思路很简单:
- 主模板:默认认为不相同,“值” 为 false。
- 当两个模板参数其实是同一种类型时,偏特化一版,改成 true。
- 读静态成员 _value 就知道结果。
下面结构体起名用 TypeEqual,避免和标准库的 std::is_same 撞名;成员按习惯用 _value。
#include <iostream>
template <typename T, typename U>
struct TypeEqual {
static constexpr bool _value = false;
};
template <typename T>
struct TypeEqual<T, T> {
static constexpr bool _value = true;
};
template <typename T, typename U>
void check_same_type()
{
if (TypeEqual<T, U>::_value)
std::cout << "T 和 U 是相同类型\n";
else
std::cout << "T 和 U 是不同类型\n";
}
int main()
{
check_same_type<int, int>();
check_same_type<int, double>();
check_same_type<char, char>();
check_same_type<int*, int*>();
return 0;
}
日常项目里直接用 std::is_same 就行;这里主要是让你看清偏特化长啥样。把泛化、特化、实例化三块串起来,读模板库源码时心里会踏实很多。