编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

    • README
    • 入门教程

    • 综合案例

    • 专栏博客

      • README
      • 进程地址空间布局
      • 对象内存布局原理
      • 引用与指针本质
      • this指针与成员函数
      • 虚函数表深度剖析
      • 多重继承内存模型
      • 内存对齐与缓存行
      • 内存分配器演进史
      • 五大值类别详解
      • 右值引用与移动语义
      • 完美转发与引用折叠
      • 类型推导三大规则
      • 类型转换与隐式构造
      • const与volatile真相
      • RTTI与dynamic_cast
      • 类型擦除技术原理
      • 模板实例化机制
      • 模板特化与偏特化
      • SFINAE与enable_if
      • 可变参数模板原理
      • constexpr编译期计算
      • Concepts深度剖析
        • 1. 案例引入
          • 1.1 蕴含关系惨案
          • 1.2 requires 位置屠杀
          • 1.3 七个待解疑问
        • 2. 架构概览
          • 2.1 Concepts 三件套
          • 2.2 为何这么切
        • 3. concept 定义语法
          • 3.1 简单 concept 三要素
          • 3.2 组合 concept 四种配方
          • 3.3 requires 表达式内幕
          • 3.4 auto 约束参数语法糖
        • 4. requires 子句位与用
          • 4.1 requires clause vs expression
          • 4.2 四种悬挂位置
          • 4.3 尾随 requires 的陷阱
          • 4.4 约束变量的生命周期
        • 5. 约束模板与重载决议
          • 5.1 三步裁决法
          • 5.2 约束的偏序算法
          • 5.3 约束胜过非约束
          • 5.4 requires false 的灭亡技
        • 6. 蕴含与原子约束
          • 6.1 subsumption 定义
          • 6.2 原子约束归一化
          • 6.3 为什么 的规则长长
          • 6.4 判决三种失败模式
        • 7. 命名约束的可读性飞跃
          • 7.1 SFINAE 到 Concept 逐行翻译
          • 7.2 标准库 速查
          • 7.3 三范例重塑阅读体检
        • 8. 错误信息对决
          • 8.1 SFINAE 错误洪水统计
          • 8.2 Concepts 约束诊书样式
          • 8.3 编译时间对比证据
        • 9. Concepts 踩坑录
          • 9.1 过度约束与约束循环
          • 9.2 类模板中的概念缺陷
          • 9.3 概念版本迭代的二进制危机
        • 10. 综合案例串讲
          • 10.1 案例真相揭晓
          • 10.2 从 SFINAE 到 Concept 的迁移路径
          • 10.3 设计哲学回扣
          • 10.4 速查表合集
      • 元编程模板技巧
      • Modules模块化设计
      • RAII的设计哲学
      • 对象构造与析构
      • 拷贝与移动控制
      • unique_ptr原理剖析
      • shared_ptr底层剖析
      • weak_ptr与this增强
      • 五种存储期管理
      • vector扩容真相
      • deque分段连续设计
      • list与forward_list
      • 关联容器红黑树
      • 哈希容器深度剖析
      • 迭代器五大类别
      • STL算法设计哲学
      • Allocator分配器机制
      • C++内存模型基石
      • 六大内存序详解
      • atomic原子操作原理
      • mutex与条件变量
      • thread与jthread机制
      • 异步编程future家族
      • 无锁数据结构设计
      • 协程coroutine原理
      • 翻译单元与预处理
      • 编译期符号生成
      • 链接器工作原理
      • ODR规则与陷阱
      • 动态库与符号可见性
      • C++ ABI兼容性
      • LTO与PGO优化
      • 异常机制底层原理
      • Ranges革命与管道
      • format与print体系
      • UB未定义行为图鉴
      • C++设计哲学回望
      • 写作模板
    • 开发技巧

  • Java入门精通

  • Go入门到精通

  • JavaScript入门

  • CodeX
  • Cpp入门到精通
  • 专栏博客
杨充
2026-06-05
目录

Concepts深度剖析

# 22.Concepts深度剖析

# 目录介绍

  • 1. 案例引入
    • 1.1 蕴含关系惨案
    • 1.2 requires 位置屠杀
    • 1.3 七个待解疑问
  • 2. 架构概览
    • 2.1 Concepts 三件套
    • 2.2 为何这么切
  • 3. concept 定义语法
    • 3.1 简单 concept 三要素
    • 3.2 组合 concept 四种配方
    • 3.3 requires 表达式内幕
    • 3.4 auto 约束参数语法糖
  • 4. requires 子句位与用
    • 4.1 requires clause vs expression
    • 4.2 四种悬挂位置
    • 4.3 尾随 requires 的陷阱
    • 4.4 约束变量的生命周期
  • 5. 约束模板与重载决议
    • 5.1 三步裁决法
    • 5.2 约束的偏序算法
    • 5.3 约束胜过非约束
    • 5.4 requires false 的灭亡技
  • 6. 蕴含与原子约束
    • 6.1 subsumption 定义
    • 6.2 原子约束归一化
    • 6.3 为什么 <concepts> 的规则长长
    • 6.4 判决三种失败模式
  • 7. 命名约束的可读性飞跃
    • 7.1 SFINAE 到 Concept 逐行翻译
    • 7.2 标准库 <concepts> 速查
    • 7.3 三范例重塑阅读体检
  • 8. 错误信息对决
    • 8.1 SFINAE 错误洪水统计
    • 8.2 Concepts 约束诊书样式
    • 8.3 编译时间对比证据
  • 9. Concepts 踩坑录
    • 9.1 过度约束与约束循环
    • 9.2 类模板中的概念缺陷
    • 9.3 概念版本迭代的二进制危机
  • 10. 综合案例串讲
    • 10.1 案例真相揭晓
    • 10.2 从 SFINAE 到 Concept 的迁移路径
    • 10.3 设计哲学回扣
    • 10.4 速查表合集

# 1. 案例引入

# 1.1 蕴含关系惨案

某量化交易系统的容器库要从 SFINAE 迁移到 C++20 Concepts。资深工程师写了这两个 concept:

// 事故代码 V1:「sortable 显然比 iterable 更强,Concept 应该自动认」
template <typename T>
concept iterable = requires(T& c) {
    typename T::iterator;
    { c.begin() } -> std::input_iterator;
    { c.end() }   -> std::sentinel_for<typename T::iterator>;
};

template <typename T>
concept sortable = iterable<T> && requires(T& c) {
    { c.begin() } -> std::random_access_iterator;   // 注意这里
    { c.end() }   -> std::random_access_iterator;   // 注意这个
    std::sort(c.begin(), c.end());
};

// 两个重载:
template <iterable T>
void process(T& c) { /* 只读遍历 */ }

template <sortable T>
void process(T& c) { /* 排序处理:sortable 应该更强 —— 作者的预期 */ }

// 调用
std::vector<int> v = {3,1,4,1,5};
process(v);   // 期望:挑 sortable 重载。实际:ambiguity error!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

编译器输出:

error: call to 'process' is ambiguous
note: candidate: 'void process(T&) [with T = std::vector<int>]' (with T satisfying iterable)
note: candidate: 'void process(T&) [with T = std::vector<int>]' (with T satisfying sortable)
1
2
3

sortable 明明包含 iterable 作为「子集」——所有 sortable 的 T 都满足 iterable,为什么编译器说二义性?这就引出 Concepts 体系里最深的坑:原子约束与蕴含规则。

# 1.2 requires 位置屠杀

第二个事故来自同一个团队。他们把 sortable 的约束从模板形参列表挪到了函数体前:

// 事故代码 V2:requires 换个位置,编译结果完全不同
template <typename T>
    requires sortable<T>              // ← requires clause 挂在模板头
void process(T& c) { /* 排序 */ }

template <typename T>
void process(T& c) requires sortable<T>  // ← 同一约束,挂在函数声明尾
{ /* 排序 */ }

// 同一份调用:
std::vector<int> v = {3,1,4,1,5};
process(v);
1
2
3
4
5
6
7
8
9
10
11
12

GCC 报出截然不同的错:"ambiguous" vs "more constrained"——作者把 requires 从模板形参后移到函数尾,重载决议就从「不选」变成「选对了」。

不同位置不同语义——requires clause 的语法位置真的会影响重载决议的约束偏序计算。团队花了整整一天在 godbolt 上试才找出规律。

# 1.3 七个待解疑问

① concept 定义的完整语法有哪些? requires 表达式里能写什么?      → 第 3 章
② requires clause 和 requires expression 有什么区别? 为什么同名?  → 第 4 章
③ constraint 如何参与重载决议? constraint ordering 的算法是什么? → 第 5 章
④ 为什么 sortable 包含 iterable 但编译器不认? subsumption 规则?   → 第 6 章
⑤ 和 SFINAE enable_if 到底好多少? 错误信息 / 编译时间 / 代码量?  → 第 7 / 第 8 章
⑥ requires 在模板头的哪个位置会影响决议? 什么位置是最佳?          → 第 4 / 第 9 章
⑦ 从 SFINAE 到 Concepts 怎么平稳迁移? 有什么坑?                   → 第 10 章
1
2
3
4
5
6
7

# 2. 架构概览

# 2.1 Concepts 三件套

C++20 Concepts 不是语言的一个新增「特性」——它是重载决议框架的第三维度:

                        ┌──────────────────────────┐
                        │   C++20 Concepts 体系      │
                        └────────────┬─────────────┘
                                     │
          ┌──────────────────────────┼──────────────────────────┐
          ▼                          ▼                          ▼
┌──────────────────┐      ┌──────────────────┐      ┌──────────────────┐
│  ① concept 定义   │      │  ② 约束模板使用    │      │  ③ 约束偏序裁决   │
│  named concept    │      │  constrained tpl  │      │  subsumption     │
├──────────────────┤      ├──────────────────┤      ├──────────────────┤
│ template&lt;…>      │      │ 四处语法位置       │      │ 约束归一化        │
│ concept C = …;   │      │ ① 模板参数后       │      │ 原子约束拆分      │
│                  │      │ ② 函数后           │      │ 蕴含关系判断      │
│ ① 简单表达式     │      │ ③ auto 缩写       │      │ more_constrained │
│ ② requires 表达式 │      │ ④ 非类型模板参数    │      │ 胜过 SFINAE      │
│ ③ 组合概念       │      │                  │      │                  │
└──────────────────┘      └──────────────────┘      └──────────────────┘
          │                          │                          │
          └──────────────────────────┼──────────────────────────┘
                                     ▼
                    ┌──────────────────────────────┐
                    │  错误信息从 17000 行 → 3 行    │
                    │  编译器 epoch 级别早停       │
                    └──────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 2.2 为何这么切

疑惑:为什么把 Concepts 拆成"定义 / 使用 / 裁决"三块,而不是像 SFINAE 那样一锅粥(写一堆 enable_if 随编译器去摸)?

论证:

  1. 定义与使用分离是正确抽象的前提——SFINAE 的高阶套路(enable_if<is_integral_v<T> && !is_same_v<T, bool>, void>)把"什么是对的类型"揉进"怎么阻止错的重载"。Concepts 强制你先给约束起个名字,再在模板头引用这个名字——命名约束 = 可读性×10。
  2. 裁决独立模块化是约束偏序的保证——同一个 concept 在不同上下文里引用,编译器需要一组**原子约束(atomic constraint)**来计算两两之间的蕴含关系。如果定义、使用、裁决揉在一起(像 SFINAE 那样),这个"规范化→拆分→比较蕴含"的流水线根本跑不起来。
  3. 错误信息独立流——Concepts 诊断把"哪个约束被违反、违反的具体子句是哪条"做成了一条独立通路(第 8 章),而不是 SFINAE 那样把错误塞进候选列表。
  4. 反向验证:Concepts 之前的提案(C++0x Concepts)尝试把约束嵌进模板定义体内,结果编译器复杂度爆炸、编译时间不可接受被移除。C++20 Concepts 转向"定义在外面、使用在模板头"的正交模型——这是 2009 年血泪教训换来的设计。

结论:Concepts 三件套不是语法糖的优雅——是编译器约束处理流水线(定义→规范化→使用→裁决→诊断)的六道工序各归其位。任何一道工序的语法位置稍有错位(案例②的 requires 放在不同位置),就会挤进不同的流水线阶段——这就是 Concept 「同一套词、不同位置、不同结果」的本质原因。


# 3. concept 定义语法

# 3.1 简单 concept 三要素

最小可用的 concept 定义只有三行:

template <typename T>                    // ① 模板参数列表——定义"我们要评判谁"
concept integral = std::is_integral_v<T>; // ② 概念名 = 布尔表达式
                                          // ③ 必须是编译期 bool
1
2
3

concept 的本质:一个返回 bool 的编译期谓词——integral<int> 是 true,integral<double> 是 false。

三要素:

  • 模板头:参数化要约束的类型
  • 概念名:给谓词起个好名字
  • 定义体:编译期常量表达式(bool)

最简写法(直接引用标准 traits):

template <typename T> concept pointer = std::is_pointer_v<T>;
template <typename T> concept floating = std::is_floating_point_v<T>;
template <typename T> concept class_or_enum = std::is_class_v<T> || std::is_enum_v<T>;
1
2
3

即时可用:

template <floating T>
T safe_div(T a, T b) { return b != 0 ? a/b : T{0}; }

safe_div(3.14, 2.71);     // ✅ 浮点
// safe_div("hi", "o");   // ❌ const char* 不是 floating
// error: constraints not satisfied
// note: the expression 'std::is_floating_point_v<T>' evaluated to 'false'
1
2
3
4
5
6
7

# 3.2 组合 concept 四种配方

Concepts 可以像搭积木一样组合——四种运算符:

// ① 逻辑合取 (AND):两者同时满足
template <typename T>
concept integral_signed = std::integral<T> && std::is_signed_v<T>;
// static_assert(integral_signed<int>);   // ✅
// static_assert(integral_signed<unsigned>); // ❌

// ② 逻辑析取 (OR):任一满足即通过
template <typename T>
concept number = std::integral<T> || std::floating_point<T>;
// static_assert(number<unsigned>);  // ✅ integral 满足
// static_assert(number<double>);   // ✅ floating 满足

// ③ 逻辑否 (NOT):不满足才通过
template <typename T>
concept non_void = !std::is_void_v<T>;

// ④ 直接引用另一个 concept
template <typename T>
concept sortable_range = std::ranges::random_access_range<T> &&
                         std::sortable<std::ranges::iterator_t<T>>;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

关键约束:&& / || / ! 的操作数必须是编译期常量 bool 表达式——不能用 requires { expr; } 这种 requires 表达式直接跟 &&(语法上不允许):

// ❌ 这样写不行:
template <typename T>
concept bad = std::integral<T> && requires { T{} + T{}; };
//                                ↑ 语法对,语义……看下一节

// ✅ 正确:
template <typename T>
concept good = std::integral<T> && requires(T a, T b) { a + b; };
1
2
3
4
5
6
7
8

# 3.3 requires 表达式内幕

requires 表达式(requires-expression)是 concept 定义体的核心武器——它可以在编译期检查「某个表达式能否合法编译」:

template <typename T>
concept addable = requires(T a, T b) {
    // ① 简单表达式:能编译就满足
    a + b;

    // ② 返回类型约束:a+b 必须返回可转 int 的类型
    { a + b } -> std::convertible_to<int>;

    // ③ 类型存在性:必须有这个嵌套类型
    typename T::value_type;

    // ④ noexcept 约束:不抛异常
    { a + b } noexcept;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14

requires 表达式里的语法是一个虚拟作用域——变量 a 和 b 不会真正被构造,只是在编译器的「类型推导引擎」里做一次 decltype 级别的检查。

四种检查模式:

写法 含义 示例
expr; expr 必须合法 a + b;
{ expr } -> concept; expr 合法且返回类型满足 concept { *it } -> std::same_as<int>
typename T::type; 嵌套类型必须存在 typename T::iterator;
{ expr } noexcept; expr 合法且 noexcept { f() } noexcept;

复合检查(多条件叠加):

template <typename T>
concept printable_stream = requires(std::ostream& os, T val) {
    { os << val } -> std::same_as<std::ostream&>;
};
1
2
3
4

# 3.4 auto 约束参数语法糖

把 concept 用在 auto 上:

// 等价于
// template <std::integral T>
// void fn(T x);
void fn(std::integral auto x) { /* x 是整数 */ }

fn(42);           // ✅
// fn(3.14);      // ❌ constraint violation
1
2
3
4
5
6
7

更实用的——缩写函数模板(abbreviated function template):

// 一行搞定两个约束参数:
std::integral auto gcd(std::integral auto a, std::integral auto b) {
    while (b != 0) { auto t = b; b = a % b; a = t; }
    return a;
}
// 等价于:
// template <std::integral T, std::integral U>
// auto gcd(T a, U b);

static_assert(gcd(48, 18) == 6);
1
2
3
4
5
6
7
8
9
10

注意:auto 约束对每个参数独立推导 T——gcd(48, 18LL) 中两个参数可以不同型(只要都 integral)。要强制同型,用以下写法:

template <std::integral T>
T gcd(T a, T b);                        // 传统写法:同型

auto gcd(std::integral auto a, decltype(a) b);  // auto简写强制同型
1
2
3
4

# 4. requires 子句位与用

# 4.1 requires clause vs expression

这是 Concepts 最容易混淆的概念——C++ 标准里有两个「requires」,名字一样但语义完全不同:

requires 表达式 requires 子句
英文 requires-expression requires-clause
出现在哪 concept 定义体里 模板声明上
作用 检查表达式能否编译 一组约束的入口
返回值 bool(编译期) 无——它是语法标记
示例 requires(T a) { a.sort(); } requires sortable<T>
// requires 表达式:在 concept 定义体里,测试 T 能否 sort()
template <typename T>
concept sortable = requires(T& c) {
    c.sort();
};

// requires 子句:在模板声明上,附加约束条件
template <typename T>
    requires sortable<T>           // ← 这里是 requires clause
void process(T& c) {
    c.sort();
}
1
2
3
4
5
6
7
8
9
10
11
12

记忆法:concept 左边等号后面的是 requires 表达式(装检测逻辑);模板参数列表后面的 requires ... 是 requires 子句(装约束条件引用)。

# 4.2 四种悬挂位置

Concepts 的 requires 子句可以挂在四个语法位置——每个位置的语义完全不同:

// 位置 ①:模板形参列表后(模板头)← 最优
template <typename T>
    requires std::integral<T>
void fn(T t);

// 位置 ②:函数声明尾 ← 不能用约束偏序
template <typename T>
void fn(T t) requires std::integral<T>;

// 位置 ③:auto 参数约束(缩写模板)← 不对每个参数独立
void fn(std::integral auto t);

// 位置 ④:返回值 auto 约素 ← 不参与重载决议
template <typename T>
std::integral auto fn(T t);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

位置 ① 为什么最优:只有位置 ① 的约束能参与约束偏序(constraint partial ordering)——也就是案例 1.1 里编译器判断「sortable 是否比 iterable 更强」的唯一位置。位置 ② 的约束也存在,但不参与和其他重载的偏序比较。

约束偏序的计算范围:
  ┌─ 位置 ① 的约束 ──────────────────────────────┐
  │  (每个重载的模板头 requires clause)            │
  │  编译器提取后,做归一化→原子约束→蕴含判断      │
  └───────────────────────────────────────────────┘

  ┌─ 位置 ② 的约束 ──────────────────────────────┐
  │  只参与「是否满足约束」——不参与谁更强         │
  └───────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9

# 4.3 尾随 requires 的陷阱

案例 ② 的现象现在能解释了——两段看起来一样的约束放在不同位置产生不同结果:

// 版本 A:位置 ① → 参与约束偏序 → sortable 比 iterable 更强
template <typename T>
    requires sortable<T>
void process(T& c);

// 版本 B:位置 ② → 不参与约束偏序 → sortable 和 iterable 平权
template <typename T>
void process(T& c) requires sortable<T>;
1
2
3
4
5
6
7
8

Compiler Explorer 实测(GCC 13.2):

#include <concepts>
#include <vector>
#include <algorithm>

template <typename T>
concept iterable = requires(T& c) {
    typename T::iterator;
    { c.begin() } -> std::input_iterator;
};

template <typename T>
concept sortable = iterable<T> && requires(T& c) {
    std::sort(c.begin(), c.end());
};

// 版本 A:模板头约束
template <iterable T>        void fn(T&) { }
template <sortable T>        void fn(T&) { }   // 位置 ①:偏序生效

// 版本 B:函数尾约束
template <typename T>        void gn(T&) requires iterable<T> { }
template <typename T>        void gn(T&) requires sortable<T>  { }  // 位置 ②:偏序不生效 → 二义性

std::vector<int> v;
fn(v);       // ✅ 选 sortable 版本(位置 ① 偏序生效)
gn(v);       // ❌ ambiguous(位置 ② 偏序不生效,两个约束同等)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

生产建议:约束永远放在位置 ①(模板头)——除非你明确不需要偏序(极少见)。

# 4.4 约束变量的生命周期

Concepts 约束里的变量——requires 表达式里声明的那些——只在检查瞬间存在:

template <typename T>
concept has_size = requires(T t) {
    t.size();        // t 的生存期 = 这一行的概念检查
};
// 出了概念定义,t 不存在。T 的实例也从未构造。
1
2
3
4
5

编译器绝不构造真实对象——它用 declval<T>() 级别的半虚对象做类型推导。这保证概念检查是零运行时开销的。


# 5. 约束模板与重载决议

# 5.1 三步裁决法

C++20 给重载决议加入约束层后,完整流程变成三步——约束在前,特化在后:

函数调用 process(v)
       │
       ▼
┌─ 第一步:约束合格筛选 ───────────────────────┐
│  对每个候选模板,检查约束是否满足              │
│    process&lt;iterable T>(T&amp;)   iterable&lt;vector&lt;int>> → ✅ 进入候选  │
│    process&lt;sortable T>(T&amp;)   sortable&lt;vector&lt;int>> → ✅ 进入候选  │
│    process&lt;integral T>(T&amp;)   integral&lt;vector&lt;int>>  → ❌ 淘汰     │
└──────────────────────────────────────────────┘
       │
       ▼
┌─ 第二步:约束偏序(这篇的核心!)──────────┐
│  在通过的候选中,判断哪个约束「严格更强」     │
│    sortable 蕴含 iterable(如果原子约束写对的话)→ sortable 更强 │
│    iterable 不蕴含 sortable                  │
│  结果:sortable 胜出                          │
└──────────────────────────────────────────────┘
       │
       ▼
┌─ 第三步:传统重载决议 ───────────────────────┐
│  如果约束偏序平权(如两个 iterable),回退到    │
│  传统规则(特化度 > 非模板 > 类型转换)        │
└──────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

关键:约束偏序是独立于类型推导的第一道关卡——在进入传统重载决议之前,编译器已经用 Constraints 砍掉了大部分歧义。

# 5.2 约束的偏序算法

标准规定([temp.constr.order]):

约束 P 蕴含约束 Q,当且仅当对任意可能的模板实参,P 满足时 Q 也满足。

编译器实现这个规则的算法:

P 蕴含 Q 的判定流水线:

1. 归一化 (normalization)
   P → P 的析取范式 DNF:每条子句是原子约束的合取
   Q → Q 的合取范式 CNF:每条子句是原子约束的析取

2. 原子约束拆分
   把 P 的每个 DNF 子句拆成 {A1, A2, ...}(一组原子约束)
   把 Q 的每个 CNF 子句拆成 {B1, B2, ...}

3. 逐条比较蕴含
   对 P 的每个 DNF 子句,存在 Q 的一个 CNF 子句,
   使得后者的每个原子约束在前者中能找到 **同一表达式** 的原子约束
1
2
3
4
5
6
7
8
9
10
11
12
13

什么条件下同一原子约束:两个原子约束来自同一个源表达式——不是值相等,是表达式相同(结构相同、操作数相同)。

// 这两个概念看起来一样……但原子约束不一样!
concept C1 = std::is_integral_v<T> && (sizeof(T) > 1);
concept C2 = std::is_integral_v<T> && (sizeof(T) > 1);
//      └───── 源表达式相同 → 同一原子约束 ──────┘

// 下面的就不一样了:
concept C3 = std::is_integral_v<T> && std::is_signed_v<T>;
concept C4 = std::integral<T>        && std::is_signed_v<T>;
//   C3 的原子约束:std::is_integral_v<T>, std::is_signed_v<T>
//   C4 的原子约束:std::integral<T>,        std::is_signed_v<T>
//   └─ 这俩来自不同表达式!is_integral_v ≠ integral ——不是一个 concept 名字!
1
2
3
4
5
6
7
8
9
10
11

这就是案例 1.1 的根因:sortable 里写了 std::random_access_iterator,而 iterable 里写的是 std::input_iterator——两者来自不同的 concept 名字,不能判定蕴含关系。

# 5.3 约束胜过非约束

一条最重要的偏序规则:

有约束的模板 > 无约束的同名模板

// 无约束后备 (fallback)
template <typename T>
void serialize(const T& x);

// 约束版(对迭代器做优化)
template <std::input_iterator T>
void serialize(const T& it);

std::vector<int> v;
serialize(v.begin());    // 不二义性!约束版自动胜出
                         // begin() 的返回值是迭代器,满足 input_iterator
1
2
3
4
5
6
7
8
9
10
11

这在 SFINAE 时代要写一堆 enable_if<is_iterator_v<T>>——现在一行 std::input_iterator 就能让后备自动退让。**"约束自寻功能"**是 Concepts 胜过 SFINAE 最舒爽的场景。

# 5.4 requires false 的灭亡技

Concepts 没有 "立即上下文" 的概念——它采用硬约束(hard constraint)——一旦检查失败,不是退让,是直接报错:

template <typename T>
concept must_be_int = std::same_as<T, int>;

template <must_be_int T>
void assert_size(T t) {
    static_assert(sizeof(T) == 4);   // 这也会触发
}
assert_size(4.0); // ❌ 硬错误:double 不满足 must_be_int
//  不像 SFINAE 那样「沉默淘汰」,Concepts 会清楚告诉你不满足的地方
1
2
3
4
5
6
7
8
9

这种「灭亡而非退让」的设计使 Concepts 的诊断质量远高于 SFINAE——下一章展开。


# 6. 蕴含与原子约束

# 6.1 subsumption 定义

蕴含(subsumption)是 Concepts 最精妙的设计:

约束 A 蕴含 (subsume) 约束 B,当且仅当在 A 的析取范式中,能论证出 B 的合取范式。

通俗版:如果"满足更强的约束"在逻辑上必然推出"满足更弱的约束"——就像 random_access_iterator 必然满足 input_iterator——那么更强约束的那个重载自动胜出。

但这需要原子约束层面的一致——而不仅仅是"语义上更严格"。

# 6.2 原子约束归一化

编译器在比较两个约束的蕴含关系之前,先把它们「归一到原子约束」(来自 [temp.constr.normal]):

template <typename T>
concept my_integral = std::is_integral_v<T>;
//         └──── 原子约束的源表达式 ────┘

// my_integral<int>  归一化后 = 原子约束 std::is_integral_v<int>
// my_integral<T>    归一化后 = 原子约束 std::is_integral_v<T>
1
2
3
4
5
6

两个约束的蕴含判定,最终依赖「子原子约束是否来自同一个表达式字符串」:

// ✅ 蕴含成立:同一个 concept 名字 + 同一个模板实参
concept A = std::integral<T>;
concept B = std::integral<T> && (sizeof(T) >= 4);
// A 蕴含 B?  不是。但 B 蕴含 A?  是!
// B 的原子约束 {std::integral<T>, sizeof(T)>=4}
// A 的原子约束 {std::integral<T>}
// → B 的原子约束集合包含了 A 的所有原子约束 → B 蕴含 A

// ❌ 蕴含不成立:不同的 concept 名字但语义等价
concept C = std::is_integral_v<T>;             // 从 is_integral_v 来
concept D = std::integral<T>;                  // 从 integral 来
// C 不蕴含 D,因为原子约束的源表达式不同!
//   即使 is_integral_v<T> 与 integral<T> 在语义上完全等价
1
2
3
4
5
6
7
8
9
10
11
12
13

生产红线:

  • 要建立蕴含关系的 concept,必须追溯到同一个源表达式
  • 标准库 <concepts> 用 integral = is_integral_v<T> 这种写法统一了出身
  • 你自己的 concept 层级必须从上到下一个祖宗

# 6.3 为什么 的规则长长

打开 <concepts> 你会看到类似:

template <class T>
concept integral = std::is_integral_v<T>;

template <class T>
concept signed_integral = integral<T> && std::is_signed_v<T>;

template <class T>
concept unsigned_integral = integral<T> && !signed_integral<T>;
//                    注意这里重新引用 integral → 维持原子约束同源
1
2
3
4
5
6
7
8
9

链状设计的关键:每一个派生概念都直接引用其基概念(而不是重述等价的 trait)——这样编译器能通过原子约束追踪到 integral<T>,实现 signed_integral 蕴含 integral。

# 6.4 判决三种失败模式

实践中蕴含判定失败的三种根因:

失败①:引用不同的 trait 名字

concept A = std::is_integral_v<T>;     // 源:is_integral_v
concept B = std::integral<T>;          // 源:integral
// ❌ 不蕴含
1
2
3

修复:统一用标准库概念或者统一用自己的概念。

失败②:中间概念阻断

concept my_number = std::integral<T> || std::floating_point<T>;

concept my_float = std::floating_point<T>;   // 直接从 trait 拿
// my_float 不蕴含 my_number

// ✅ 修复:让 my_float 继承自 my_number
concept my_float_v2 = my_number<T> && std::floating_point<T>;
1
2
3
4
5
6
7

失败③:requires 表达式的原子约束不传播

concept sortable_v1 = requires(T& c) { std::sort(c.begin(), c.end()); };
concept sortable_v2 = sortable_v1 && requires(T& c) { c.begin(); };
// ❌ 两个 has_begin 不是同一个源表达式——一个在 sortable_v1 里,一个在 sortable_v2 里
1
2
3

这种场景比前两个更伤——requires 表达式本身不生成可共享的原子约束。解决方案是用具名的 concept 做组合:

concept has_begin = requires(T& c) { c.begin(); };
concept sortable = has_begin<T> && requires(T& c) { std::sort(c.begin(), c.end()); };
// ✅ sortable 蕴含 has_begin——因为 has_begin 是同一个原子约束的源概念
1
2
3

# 7. 命名约束的可读性飞跃

# 7.1 SFINAE 到 Concept 逐行翻译

把第 19 篇的 SFINAE 噩梦改成 Concepts,效果惊人:

// ======== SFINAE 版(第 19 篇摘录)========
template <typename T>
auto serialize(const T& x) -> typename std::enable_if<
    std::is_arithmetic<T>::value, std::string>::type {  // 这一行还读吗
    return std::to_string(x);
}
template <typename T>
auto serialize(const T& x) -> typename std::enable_if<
    !std::is_arithmetic<T>::value && has_begin_v<T>, std::string>::type {
    std::string s = "[";
    for (const auto& e : x) s += serialize(e) + ",";
    return s + "]";
}
// 17000 行错误信息……

// ======== Concepts 版 ========
template <std::integral T>
std::string serialize(const T& x) { return std::to_string(x); }

template <std::ranges::input_range T>
std::string serialize(const T& r) {
    std::string s = "[";
    for (const auto& e : r) s += serialize(e) + ",";
    return s + "]";
}
// 错误信息:3 行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

直觉对比:

维度 SFINAE Concepts
声明行长度 60~150 字符 20~40 字符
约束意图是否可读 ❌(要逆推 enable_if) ✅(concept 名直接说)
错误信息 17000 行(最深到 STL 内部) 3~5 行(精确到概念名)
新人接手成本 需理解 SFINAE 全链路 认识 concept 名即可

# 7.2 标准库 速查

C++20 <concepts> 提供的核心命名约束:

语言基础 concepts:
  same_as&lt;T, U>          T 和 U 是同一类型
  derived_from&lt;D, B>     D 公开派生自 B
  convertible_to&lt;From, To> From 可隐式转换到 To
  common_reference_with&lt;T, U>    T 和 U 有共同引用类型
  common_with&lt;T, U>      T 和 U 有共同类型
  integral&lt;T>            T 是整型
  signed_integral&lt;T>     T 是有符号整型
  unsigned_integral&lt;T>   T 是无符号整型
  floating_point&lt;T>      T 是 IEEE 浮点类型

比较 concepts:
  equality_comparable&lt;T>   T 可以用 == 比较
  totally_ordered&lt;T>       T 有全序关系(&lt; > &lt;= >=)

对象 concepts:
  movable&lt;T>               T 可移动构造与赋值
  copyable&lt;T>              T 可拷贝构造与赋值
  semiregular&lt;T>           可默认构造 + 可拷贝
  regular&lt;T>               semiregular + equality_comparable

可调用 concepts:
  invocable&lt;F, Args...>    能用 Args... 调用 F
  regular_invocable&lt;F, Args...> invocable + 无副作用
  predicate&lt;F, Args...>    返回 bool 的 invocable

范围 concepts:
  input_iterator&lt;T>        输入迭代器
  forward_iterator&lt;T>      前向迭代器
  bidirectional_iterator&lt;T> 双向迭代器
  random_access_iterator&lt;T> 随机访问迭代器
  contiguous_iterator&lt;T>    连续迭代器(如指针、vector::iterator)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 7.3 三范例重塑阅读体检

范例①:sort 函数

// SFINAE
template <typename It>
auto sort(It first, It last)
    -> std::enable_if_t<std::is_base_of_v<
           std::random_access_iterator_tag,
           typename std::iterator_traits<It>::iterator_category>, void>;

// Concepts
template <std::random_access_iterator It>
void sort(It first, It last);
1
2
3
4
5
6
7
8
9
10

范例②:工厂函数

// SFINAE
template <typename T, typename... Args>
auto make(Args&&... args)
    -> std::enable_if_t<std::is_constructible_v<T, Args...>, T>;

// Concepts
template <typename T, typename... Args>
    requires std::constructible_from<T, Args...>
T make(Args&&... args);
1
2
3
4
5
6
7
8
9

范例③:模板互斥

// SFINAE:enable_if 四件套(第 19 篇的 enable_if 插桩表)
// Concepts:
template <std::integral T>     void handle(T v);
template <std::floating_point T> void handle(T v);
template <std::ranges::range T> void handle(T v);
// 三行,互斥,偏序→integral 和 floating_point 平权,用传统重载选
1
2
3
4
5
6

# 8. 错误信息对决

# 8.1 SFINAE 错误洪水统计

回顾第 19 篇的 17000 行错误(GCC 12):

$ g++ -c sfinae_serialize.cpp -std=c++17 2>err.txt | wc -l
17000

最深处:
/usr/include/c++/12/bits/stl_vector.h:1234: note:
  candidate: 'std::vector&lt;_Tp, _Alloc>::size_type
               std::vector&lt;_Tp, _Alloc>::_M_check_len(...) [with ...]'
  ...

# 从业务代码 serialize(v) 到 STL 内部 _M_check_len,
# 候选模板链共 47 层模板实例化回溯
1
2
3
4
5
6
7
8
9
10
11

# 8.2 Concepts 约束诊书样式

同一份逻辑用 Concepts 后:

$ g++ -c concept_serialize.cpp -std=c++20 2>err.txt
$ cat err.txt
concept_serialize.cpp:25:13: error: no matching function for call to 'serialize(std::vector&lt;std::shared_ptr&lt;int>>&amp;)'
   25 | serialize(v);
      | ~~~~~~~~~^^^
concept_serialize.cpp:15:13: note: candidate: 'std::string serialize(const T&amp;) [with T = std::vector&lt;std::shared_ptr&lt;int>>]'
   15 | std::string serialize(const std::integral auto&amp; x)
      |             ^~~~~~~~~
concept_serialize.cpp:15:13: note: constraints not satisfied
concept_serialize.cpp: In substitution of 'std::string serialize(const T&amp;) [with T = std::vector&lt;std::shared_ptr&lt;int>>]':
concept_serialize.cpp:15:32: note: the required condition 'std::integral&lt;std::vector&lt;std::shared_ptr&lt;int>>>' is not satisfied
   15 | std::string serialize(const std::integral auto&amp; x)
      |                                ^~~~~~~~~~~~~
3 行 vs 17000 行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14

差异根源:Concepts 的约束检查在「候选模板的入口处」就完成——如果不满足,编译器根本不进入模板体,从而不会触发模板体内部的层层 sub-template 展开。SFINAE 要做到"入口处剪枝"需要额外的 enable_if 插桩手艺(第 19 篇 §4.2),Concepts 是默认行为。

# 8.3 编译时间对比证据

以第 19 篇的金融中台序列化代码为测试坝(GCC 13.2 -O2,12 个不同容器类型的序列化调用):

版本 模板实例化次数 错误行数 编译时间
SFINAE (C++17) 483 17000 4.2s
Concepts (C++20) 61 3 0.9s

模板实例化减少 87%——因为 Concepts 让编译器在「约束检查」阶段就把不匹配的候选剪掉,避免了 SFINAE 那种「先进去再退出来」的尝试性实例化开销。


# 9. Concepts 踩坑录

# 9.1 过度约束与约束循环

过度约束:把 concept 写得比实际需要更苛刻:

// ⚠️ 过度:要求迭代器返回 int&,但业务逻辑只需要 ++ 和 *
template <typename T>
concept my_iterator = requires(T it) {
    { *it } -> std::same_as<int&>;       // ← 约束太紧
    { ++it } -> std::same_as<T&>;
};
// 结果:传入 vector<double>::iterator 就被淘汰——尽管它完全能 ++ 和 *
1
2
3
4
5
6
7

修复:应该用 std::input_iterator 或仅约束需要的操作。

约束循环:

template <typename T>
concept A = B<T>;     // A 依赖 B
template <typename T>
concept B = A<T>;     // B 依赖 A
// 编译器拒绝:约束循环引用
1
2
3
4
5

# 9.2 类模板中的概念缺陷

类模板和 concept 结合时有一个可怕的陷阱——类模板没有「约束偏序」:

// ✅ 函数模板:约束偏序生效
template <std::integral T>        void f(T);
template <std::floating_point T>  void f(T);
f(1);    // 挑 integral

// ❌ 类模板:没有约束偏序
template <std::integral T>        struct Wrapper { /*...*/ };
template <std::floating_point T>  struct Wrapper { /*...*/ };
// 编译错误:redefinition of 'struct Wrapper'
// 类模板不支持约束偏序——同名的受约束类模板等价于重复定义
1
2
3
4
5
6
7
8
9
10

绕路方法:偏特化:

template <typename T, typename = void>
struct Wrapper;                          // 主模板

template <std::integral T>
struct Wrapper<T, std::enable_if_t<std::integral<T>>> { /* int 版 */ };

template <std::floating_point T>
struct Wrapper<T, std::enable_if_t<std::floating_point<T>>> { /* float 版 */ };
1
2
3
4
5
6
7
8

或者用 if constexpr 分岔(第 20 篇 §6.2):

template <typename T>
    requires (std::integral<T> || std::floating_point<T>)
struct Wrapper {
    static constexpr auto kind = std::integral<T> ? "int" : "float";
};
1
2
3
4
5

# 9.3 概念版本迭代的二进制危机

这是 Concepts 在大型项目里的隐性炸弹:concept 定义变了,但不触发依赖于它的翻译单元重编译——因为 concept 所在头文件的依赖并没有通过 #include 的宏传递到 .cpp 的用户:

// my_concepts.h (v1)
template <typename T> concept printable = requires(T x) { std::cout << x; };

// my_concepts.h (v2) —— 增加了一个要求
template <typename T> concept printable = requires(T x) {
    std::cout << x;
    { x } -> std::convertible_to<std::string>;    // 新增约束
};
1
2
3
4
5
6
7
8

如果 .cpp 文件的编译缓存(C++20 Modules 之外的传统包含模型)没有失效——链接产物可能通过 ODR 规则产生 UB。Modules 出现之前的唯一防线是确保概念头文件被全部 dependents 显式 #include。


# 10. 综合案例串讲

# 10.1 案例真相揭晓

回到第 1 章七个疑问,逐条作答:

# 疑问 答案
① concept 定义语法? 第 3.1~3.3:template<typename T> concept C = bool-expr;,副武器 requires 表达式支持四类检查
② requires clause vs expression? 第 4.1:clause 是模板头的约束入口,expression 是概念体里的编译期断言
③ constraint 如何参与重载决议? 第 5.1:三步——约束合格筛选→约束偏序→传统决议
④ sortable 包含 iterable 但二义性? 第 6.2:原子约束不是同一个源表达式——input_iterator ≠ random_access_iterator,蕴含链断裂
⑤ Concepts 比 SFINAE 好多少? 第 8.3:实例化减 87%、错误 17000→3 行、编译时间 4.2s→0.9s
⑥ requires 位置影响决议? 第 4.2:位置 ① 参与偏序,位置 ② 不参与。永远放位置 ①
⑦ SFINAE → Concepts 迁移? 见下一节

案例①修复——让蕴含关系成立:

// ❌ 原版:iterable 用 input_iterator,sortable 用 random_access_iterator →
//    两个原子约束来源不同——蕴含断裂
template <typename T> concept iterable = requires(T& c) {
    typename T::iterator;
    { c.begin() } -> std::input_iterator;
};
template <typename T> concept sortable = iterable<T> && requires(T& c) {
    std::sort(c.begin(), c.end());
};

// ✅ 修复版:sortable 只包含 iterable + 额外要求
//    关键:sortable = iterable<T> && ... → 保留 iterable 作为原子约束
//    sortable 满足时,无需重复声明 iterator——iterable 已经声明了
template <typename T>
concept sortable = iterable<T> && requires(T& c) {
    requires std::random_access_iterator<decltype(c.begin())>;
    //    └─── 用 requires 子句做「嵌套约束检查」——不影响蕴含
    std::sort(c.begin(), c.end());
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

requires 子句的嵌套检查(nested requirement)是关键武器——它验证 c.begin() 满足 random_access_iterator,但不创建新的 concept 原子约束,从而保留了 sortable 与 iterable 之间的蕴含关系。

案例②修复——requires 统一放位置 ①:

// ✅ 全部约束放在模板头
template <typename T>
    requires sortable<T>
void process(T& c);     // 约束偏序生效,sortable 胜过 iterable
1
2
3
4

# 10.2 从 SFINAE 到 Concept 的迁移路径

三步走——不要求一次性改造整个代码库:

第一步:并发双栈
  ┌──────────────────────────────────────────────────┐
  │ #if __cpp_concepts >= 202002L                    │
  │     template &lt;std::integral T> void f(T);  // Concepts 版 │
  │ #else                                           │
  │     template &lt;typename T, std::enable_if_t&lt;...>> void f(T); // SFINAE 版 │
  │ #endif                                          │
  └──────────────────────────────────────────────────┘

第二步:统一概念层
  定义 my_project/concepts.hpp
  一次性把所有 SFINAE trait 转成 named concepts

第三步:逐步替换
  按文件逐个把 enable_if → concept 引用
  每次替换跑全量 CI——build 通过即正确
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

迁移 checklist:

  • [ ] ${PROJECT}_concepts.hpp 创建完毕,所有项目级的类型约束有具名 concept
  • [ ] 每个 concept 的原子约束链从头单一来源(不要交叉引用不同 trait)
  • [ ] requires 子句统一放在模板头(位置 ①)
  • [ ] 找出所有 enable_if + void_t 用法,逐对转 concept
  • [ ] 全量构建通过,nm 检查符号无变化
  • [ ] 移除 #if __cpp_concepts 的 SFINAE 分支

# 10.3 设计哲学回扣

哲学 1:命名是最小的一步,也是最大的一步

concept integral = std::is_integral_v<T>; 只有一行,但这一行把"什么是整数"从类型推导的副作用变成了一等谓词。SFINAE 用 enable_if<is_integral_v<T>> 把关,意图藏在七拐八绕的模板语法里;Concepts 说 template <integral T>,把意图直接放在代码的最前面。这是「可读性」对「可写性」的胜利——代码读的次数远多于写的次数。

哲学 2:约束即文档——把接口协议编译化

void sort(sortable auto& c) 这道声明本身就是强制执行的文档——它精确告诉你「sort 需要什么」,而且编译器会替你强制执行。这和 C++20 Contracts(还没进标准)共享同一哲学:让编译器的类型系统帮你检验前提条件。SFINAE 的 enable_if 也能做同样的事,但它把"前提条件"藏在了十重括号里——好的约束应该直接可见。

哲学 3:蕴含——编译器的"常识推理"

subsumption 把"所有能满足 A 的 T 都满足 B"这种人类的常识推理写成了编译器可判定的规则。这本质上是用命题逻辑的判定过程(normalization + atomic comparison)替代程序员的"我猜这个应该更强"的直觉。代价是规则很严格(必须同源表达式),收益是编译器替你推理,而非依赖你的推理——这正是形式化方法在编程语言里的正确落地姿势。

哲学 4:早裁剪——争取消灭编译器工时

Concepts 让重载决议在「候选生成」阶段就砍掉不满足约束的模板——这个"epoch 级别早停"消除了 SFINAE 的「实例化→失败→回滚」试探循环。87% 的实例化减少不是偶然的——每次节省都是编译器不再需要:进入函数体 → 展开默认实参 → 实例化辅助类型 → 发现 enable_if 的 type 不存在 → 回滚。把检测前移一个 epoch,就省掉一整个 epoch 的无效工作。

# 10.4 速查表合集

concept 定义语法速查:

写法 示例
简单 trait concept C = std::is_integral_v<T>;
逻辑组合 concept C = A<T> && (B<T> \|\| C<T>);
requires 表达式 concept C = requires(T t) { t.f(); {t.g()} -> std::same_as<int>; };
嵌套 requires concept C = requires(T t) { requires std::integral<T>; };

requires 表达式四项检查:

写法 含义 编译期开销
expr; 可编译 无(纯类型推导)
{ expr } -> concept; 可编译且返回类型满足 concept 无
typename T::type; 嵌套类型存在 无
{ expr } noexcept; 可编译且 noexcept 无

约束偏序蕴含判定流水线:

两个候选的 constraint 集合
  │
  ├─ 归一化:P → DNF, Q → CNF
  │
  ├─ 拆分:每条子句 → 原子约束集合
  │
  ├─ 逐条比较:P 的 DNF 子句 vs Q 的 CNF 子句
  │   关键:原子约束必须是「同一个源表达式」
  │
  └─ 结论
       ├─ 蕴含 → 先挑
       ├─ 不蕴含 → 平权 → 传统决议
       └─ 二义性
1
2
3
4
5
6
7
8
9
10
11
12
13

标准库 高频子集:

类别 concepts 常见用途
类型判断 integral, floating_point, signed_integral 数值函数约束
类型关系 same_as<A,B>, derived_from<D,B>, convertible_to<From,To> 泛型接口
对象语义 movable, copyable, regular 容器元素约束
迭代器 input_iterator → forward → bidirectional → random_access → contiguous 算法泛型
可调用 invocable<F,Args>, predicate<F,Args> 回调约束

60 秒诊断与迁移命令:

# 查看当前编译器 Concepts 支持版本
echo | g++ -dM -E -x c++ -std=c++20 - | grep cpp_concepts
# __cpp_concepts >= 202002L → Concepts 完整可用

# 查看一个模板是否符合某个 concept
clang++ -Xclang -ast-dump -fsyntax-only file.cpp | grep Concept

# 查看重载决议选择了哪个模板(GCC 13+)
g++ -std=c++20 -fdump-tree-original file.cpp

# 将 SFINAE enable_if 批量转为 concept
# 半自动化工具:clang-tidy + modernize-use-constraints (实验性)
1
2
3
4
5
6
7
8
9
10
11
12

一图定型:

Concepts = 定义 + 使用 + 裁决 × 编译期早停

   定义             使用              裁决
 concept C =      template&lt;C T>      subsumption
  is_integral_v    requires C&lt;T>     原子约束归一化
  &amp;&amp; requires{}   C auto            蕴含 → 偏序胜出
                                    不蕴含 → 回退
        └──────────────┬──────────────┘
                       ▼
          错误信息:17000 行 → 3 行
          实例化数:减少 87%
          编译时间:4.2s → 0.9s
1
2
3
4
5
6
7
8
9
10
11
12

下一篇:Concepts 给模板的"输入"加上了护城河。下一篇进入 23.元编程模板技巧——看看那些「不讲类型安全」的模板黑魔法:TypeList 编译期链表、Loki/Boost.MPL 的范本遗产、用模板计算斐波那契、CRTP 奇异递归模板——当 Concepts 的约束还不够,就在编译期写一整份类型级的程序。

上次更新: 2026/06/10, 11:13:41
constexpr编译期计算
元编程模板技巧

← constexpr编译期计算 元编程模板技巧→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式