模板、实例化与 SFINAE / Templates, Instantiation, and SFINAE
模板不是“能接收任意类型的普通函数”。
模板更像一张生成代码的配方。只有被具体类型使用时,编译器才会根据配方生成对应代码。
1. 函数模板不是函数本身
看代码:
template <class T>
T add(T a, T b) {
return a + b;
}
int main() {
int x = add(1, 2);
}add 这里是函数模板。
当你用两个 int 调用它时,编译器会生成一个类似这样的函数:
int add<int>(int a, int b) {
return a + b;
}流程:
function template add<T>
|
v
call add(1, 2)
|
v
deduce T = int
|
v
instantiate add<int>
|
v
compile generated function body如果你从来没有用 double 调用 add,编译器通常不会为 double 生成一份 add<double>。
所以:
模板定义
是模式
模板实例化
是用具体类型生成具体代码2. 模板错误为什么常常很晚出现
看代码:
template <class T>
void print_size(T value) {
std::cout << value.size() << '\n';
}模板定义本身看起来像能通过。
但它隐含一个要求:
T 类型的对象必须能调用 .size()当你这样调用:
std::string s = "hello";
print_size(s);std::string 有 size(),可以。
当你这样调用:
print_size(42);编译器实例化 print_size<int> 时,才发现:
int 没有 size()模板错误常常很长,就是因为错误发生在“生成代码并检查”的过程中。
调用链可能一层套一层。
3. 模板参数推导
函数模板调用时,编译器会从实参推导模板参数。
template <class T>
void show(T value);
show(42); // T = int
show(3.14); // T = double如果两个参数都要求同一个 T:
template <class T>
T max_value(T a, T b) {
return a < b ? b : a;
}调用:
max_value(1, 2); // T = int
max_value(1.0, 2.0); // T = double但:
max_value(1, 2.0);可能推导失败。
因为第一个参数希望 T = int,第二个参数希望 T = double。
你可以显式指定:
max_value<double>(1, 2.0);这时 1 可以转换成 double。
4. 模板不是宏
模板和宏都能产生“看起来泛化”的代码。
但它们不同。
宏主要是预处理阶段的文本替换:
#define MAX(a, b) ((a) < (b) ? (b) : (a))模板是 C++ 语言层面的类型机制:
template <class T>
T max_value(T a, T b) {
return a < b ? b : a;
}模板知道类型。
编译器可以对模板实例化后的代码做类型检查、重载解析和优化。
所以现代 C++ 泛型代码主要靠模板,而不是宏。
5. SFINAE 是什么
SFINAE 是 Substitution Failure Is Not An Error。
可以翻译成:
模板参数替换失败,不一定立刻成为硬错误。它只在特定模板替换语境中成立。
最常见的效果是:某个重载候选因为替换失败被移除,然后编译器尝试其他候选。
看一个检测 .size() 的例子:
#include <type_traits>
template <class T>
auto has_size(T const& x) -> decltype(x.size(), std::true_type{}) {
return {};
}
std::false_type has_size(...) {
return {};
}如果传入 std::string:
尝试第一个 has_size
|
v
x.size() 合法
|
v
第一个候选可用,返回 true_type如果传入 int:
尝试第一个 has_size
|
v
x.size() 不合法
|
v
替换失败,候选被移除
|
v
选择 ... 兜底版本,返回 false_type这就是 SFINAE 的基本感觉。
6. void_t:把表达式是否合法变成类型判断
常见写法:
#include <type_traits>
#include <utility>
template <class, class = void>
struct has_size : std::false_type {};
template <class T>
struct has_size<T, std::void_t<decltype(std::declval<T>().size())>>
: std::true_type {};先不用害怕这段代码。
拆开看:
std::declval<T>()
在类型语境中假装有一个 T 对象
decltype(std::declval<T>().size())
检查 T 对象能否调用 size()
std::void_t<...>
如果里面的类型都合法,结果是 void
替换失败
选择主模板 false_type
替换成功
选择偏特化 true_type这类技巧曾经大量用于模板库。
它能工作,但可读性一般,报错也不总是友好。
7. 现代代码为什么更喜欢 concept
SFINAE 的问题是:
技巧性强
错误信息长
约束藏在返回类型或辅助模板里
读者不容易看出接口要求C++20 的 concept 把要求写得更直接:
template <class T>
concept HasSize = requires(T x) {
x.size();
};这表达:
T 类型的对象 x 必须能调用 x.size()下一篇会专门讲 concept。
本篇先记住:concept 是对 SFINAE 时代很多约束技巧的更清晰表达。
8. 本篇总结
模板学习先抓住这条线:
模板定义
|
v
模板参数推导
|
v
实例化具体代码
|
v
检查实例化后的代码是否合法SFINAE 的核心是:
在替换模板参数时失败
|
v
某些情况下不是硬错误
|
v
候选被移除,重载解析继续不要一上来背复杂模板技巧。
先能解释:模板什么时候生成代码,错误为什么在实例化时出现,SFINAE 为什么能做“检测某个表达式是否合法”。