Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← C++ 编程 / C++ Programming

现代 C++ / Modern C++

1. 现代 C++ 总览:不是特性清单,而是表达能力 / Modern C++ Overview

2. 移动、转发与值类别:std::move 为什么不移动 / Move, Forward, and Value Categories

3. 类型推导、初始化与 constexpr / Type Deduction, Initialization, and constexpr

4. 模板、实例化与 SFINAE / Templates, Instantiation, and SFINAE

5. concept 与约束:把模板要求写在接口上 / Concepts and Constraints

6. 异常、RAII 与 noexcept:失败路径也要清理 / Exceptions, RAII, and noexcept

7. lambda、捕获与生命周期:回调不是免费午餐 / Lambdas, Captures, and Lifetime

8. callable wrapper 与 std::invoke:怎样保存和调用行为 / Callable Wrappers and std::invoke

9. 词汇类型与错误建模:让接口自己说话 / Vocabulary Types and Error Modeling

10. 协程与异步抽象:先理解状态机 / Coroutines and Async Abstractions

本页目录

模板、实例化与 SFINAE / Templates, Instantiation, and SFINAE ​

模板不是“能接收任意类型的普通函数”。

模板更像一张生成代码的配方。只有被具体类型使用时,编译器才会根据配方生成对应代码。

1. 函数模板不是函数本身 ​

看代码:

cpp
template <class T>
T add(T a, T b) {
    return a + b;
}

int main() {
    int x = add(1, 2);
}
1
2
3
4
5
6
7
8

add 这里是函数模板。

当你用两个 int 调用它时,编译器会生成一个类似这样的函数:

cpp
int add<int>(int a, int b) {
    return a + b;
}
1
2
3

流程:

text
function template add<T>
  |
  v
call add(1, 2)
  |
  v
deduce T = int
  |
  v
instantiate add<int>
  |
  v
compile generated function body
1
2
3
4
5
6
7
8
9
10
11
12
13

如果你从来没有用 double 调用 add,编译器通常不会为 double 生成一份 add<double>。

所以:

text
模板定义
  是模式

模板实例化
  是用具体类型生成具体代码
1
2
3
4
5

2. 模板错误为什么常常很晚出现 ​

看代码:

cpp
template <class T>
void print_size(T value) {
    std::cout << value.size() << '\n';
}
1
2
3
4

模板定义本身看起来像能通过。

但它隐含一个要求:

text
T 类型的对象必须能调用 .size()
1

当你这样调用:

cpp
std::string s = "hello";
print_size(s);
1
2

std::string 有 size(),可以。

当你这样调用:

cpp
print_size(42);
1

编译器实例化 print_size<int> 时,才发现:

text
int 没有 size()
1

模板错误常常很长,就是因为错误发生在“生成代码并检查”的过程中。

调用链可能一层套一层。

3. 模板参数推导 ​

函数模板调用时,编译器会从实参推导模板参数。

cpp
template <class T>
void show(T value);

show(42);     // T = int
show(3.14);   // T = double
1
2
3
4
5

如果两个参数都要求同一个 T:

cpp
template <class T>
T max_value(T a, T b) {
    return a < b ? b : a;
}
1
2
3
4

调用:

cpp
max_value(1, 2);      // T = int
max_value(1.0, 2.0);  // T = double
1
2

但:

cpp
max_value(1, 2.0);
1

可能推导失败。

因为第一个参数希望 T = int,第二个参数希望 T = double。

你可以显式指定:

cpp
max_value<double>(1, 2.0);
1

这时 1 可以转换成 double。

4. 模板不是宏 ​

模板和宏都能产生“看起来泛化”的代码。

但它们不同。

宏主要是预处理阶段的文本替换:

cpp
#define MAX(a, b) ((a) < (b) ? (b) : (a))
1

模板是 C++ 语言层面的类型机制:

cpp
template <class T>
T max_value(T a, T b) {
    return a < b ? b : a;
}
1
2
3
4

模板知道类型。

编译器可以对模板实例化后的代码做类型检查、重载解析和优化。

所以现代 C++ 泛型代码主要靠模板,而不是宏。

5. SFINAE 是什么 ​

SFINAE 是 Substitution Failure Is Not An Error。

可以翻译成:

text
模板参数替换失败,不一定立刻成为硬错误。
1

它只在特定模板替换语境中成立。

最常见的效果是:某个重载候选因为替换失败被移除,然后编译器尝试其他候选。

看一个检测 .size() 的例子:

cpp
#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 {};
}
1
2
3
4
5
6
7
8
9
10

如果传入 std::string:

text
尝试第一个 has_size
  |
  v
x.size() 合法
  |
  v
第一个候选可用,返回 true_type
1
2
3
4
5
6
7

如果传入 int:

text
尝试第一个 has_size
  |
  v
x.size() 不合法
  |
  v
替换失败,候选被移除
  |
  v
选择 ... 兜底版本,返回 false_type
1
2
3
4
5
6
7
8
9
10

这就是 SFINAE 的基本感觉。

6. void_t:把表达式是否合法变成类型判断 ​

常见写法:

cpp
#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 {};
1
2
3
4
5
6
7
8
9

先不用害怕这段代码。

拆开看:

text
std::declval<T>()
  在类型语境中假装有一个 T 对象

decltype(std::declval<T>().size())
  检查 T 对象能否调用 size()

std::void_t<...>
  如果里面的类型都合法,结果是 void

替换失败
  选择主模板 false_type

替换成功
  选择偏特化 true_type
1
2
3
4
5
6
7
8
9
10
11
12
13
14

这类技巧曾经大量用于模板库。

它能工作,但可读性一般,报错也不总是友好。

7. 现代代码为什么更喜欢 concept ​

SFINAE 的问题是:

text
技巧性强
错误信息长
约束藏在返回类型或辅助模板里
读者不容易看出接口要求
1
2
3
4

C++20 的 concept 把要求写得更直接:

cpp
template <class T>
concept HasSize = requires(T x) {
    x.size();
};
1
2
3
4

这表达:

text
T 类型的对象 x 必须能调用 x.size()
1

下一篇会专门讲 concept。

本篇先记住:concept 是对 SFINAE 时代很多约束技巧的更清晰表达。

8. 本篇总结 ​

模板学习先抓住这条线:

text
模板定义
  |
  v
模板参数推导
  |
  v
实例化具体代码
  |
  v
检查实例化后的代码是否合法
1
2
3
4
5
6
7
8
9
10

SFINAE 的核心是:

text
在替换模板参数时失败
  |
  v
某些情况下不是硬错误
  |
  v
候选被移除,重载解析继续
1
2
3
4
5
6
7

不要一上来背复杂模板技巧。

先能解释:模板什么时候生成代码,错误为什么在实例化时出现,SFINAE 为什么能做“检测某个表达式是否合法”。

最后更新于:

Pager
上一篇3. 类型推导、初始化与 constexpr / Type Deduction, Initialization, and constexpr
下一篇5. concept 与约束:把模板要求写在接口上 / Concepts and Constraints

持续记录,持续成长

Copyright © Tidenflow