52 / 80 · C++11 · 约 10 分钟
全特化、偏特化与重载不要混用
全特化为确定的模板实参提供替代定义,偏特化为一类实参模式提供实现。类模板可以偏特化,函数模板不能;函数行为的分流通常优先用重载或约束,避免误判特化参与选择的方式。
一个具体点与一类模式
主模板给出默认规则;template<> struct Kind<int> 是全特化,只对应完整确定的一组实参;template<class T> struct Kind<T*> 是偏特化,匹配所有符合指针模式的实参。偏特化不一定意味着只填写部分参数,也可以对所有参数增加相互关系或结构限制。
示例把普通类型、指针类型和精确的 int 类型分为三类。Kind<double> 使用主模板,Kind<double*> 匹配指针偏特化,Kind<int> 使用显式全特化。输出来自所选定义中的静态函数,不是运行期检查类型后再进行分支。
特化是独立定义,不自动继承成员
类模板的特化是替代定义,不会因为主模板有某个成员,就自动获得那个成员。若公共接口需要保持一致,应由每个定义主动提供;也可以抽取真正共享的实现,但不要为了少写几行而把类型选择结构绕得难以阅读。
多个偏特化同时匹配时,编译器用偏序规则寻找更特化的一项;若无法比较出唯一最佳者,就产生歧义。不能把它理解成源码里“后写的覆盖前写的”。主模板应先声明,相关特化还必须在会使用它的实例化位置可见,否则跨文件行为可能出问题。
函数通常优先使用重载
函数模板支持显式全特化,却不支持偏特化。想专门处理 T*,通常直接增加一个参数为 T* 的函数模板重载;C++20 还可用 concepts 表达更清晰的候选条件。全特化不作为独立重载候选参加最初的重载选择,这使重载与特化混合时容易偏离直觉。
本例使用 type_traits 的 static_assert 为声明版本建立 C++11 基线,而模板特化本身早于 C++11。实际项目优先选择最局部的工具:一种类型改变存储布局时考虑类特化;一组类型走不同算法时考虑重载、约束或 if constexpr。
容易答错的地方
- 函数模板不能偏特化;看起来像
f<T*>(...)的尝试不等于合法的函数模板重载声明。 - 不要向 std 随意添加特化;标准库仅在明确允许且满足要求的场景接受用户特化,很多 traits 明确禁止这样做。
运行一个例子
最低标准 C++11 · 完整程序 · 下载 .cpp
#include <cassert>
#include <iostream>
#include <type_traits>
template<class T>
struct Kind { static int code() { return 0; } };
template<class T>
struct Kind<T*> { static int code() { return 1; } };
template<>
struct Kind<int> { static int code() { return 2; } };
int main() {
static_assert(std::is_pointer<double*>::value, "pointer case");
assert(Kind<double>::code() == 0);
assert(Kind<double*>::code() == 1);
assert(Kind<int>::code() == 2);
assert(Kind<const int*>::code() == 1);
std::cout << Kind<double>::code() << ' ' << Kind<double*>::code()
<< ' ' << Kind<int>::code() << '\n';
}
在本地编译
g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread modern-specialization.cpp -o example && ./example预期结果
0 1 2
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
Kind<int*> 会选 Kind<int> 的全特化吗?若只想对 int* 返回 3,应该增加什么?
查看参考答案
不会,int* 与 int 是不同类型,现有匹配结果是指针偏特化,返回 1。增加 template<> struct Kind<int*> { static int code() { return 3; } };,并放在第一次相关实例化之前,就为 int* 提供精确替代定义;其他指针仍走偏特化。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。