C++ 约束模板参数Concepts详解
一、Concepts的概念与用法
1、概念是什么
C++ Concepts 是 C++20 引入的一套“模板参数约束机制”。它的核心作用是:
- 明确描述模板参数必须满足什么能力
- 让模板报错更早、更清晰
- 让重载选择更符合直觉
- 替代很多过去用 SFINAE、enable_if、检测惯用法硬凑出来的写法
一句话理解:
以前你只能写“这个模板接受任意类型”,等实例化时报一大串错误。
现在你可以先声明“这个模板只接受可比较、可拷贝、可迭代的类型”。
例如,过去你可能写:
template<typename T> auto max_value(const T& a, const T& b) { return a < b ? b : a; }如果 T 不支持 <,错误往往出现在模板深处,信息很差。
用了 Concept 之后可以写:
#include <concepts> template<typename T> concept LessThanComparable = requires(const T& a, const T& b) { { a < b } -> std::convertible_to<bool>; }; template<LessThanComparable T> const T& max_value(const T& a, const T& b) { return a < b ? b : a; }这时约束不满足,编译器会直接告诉你:
这个类型不满足 LessThanComparable。
2、为什么它重要
Concepts 解决的是模板编程的三个老问题。
可读性差
你从函数签名根本看不出模板对类型有什么要求。错误信息差
错误常常是几十行甚至几百行模板展开栈。重载控制弱
多个模板重载之间很难表达“更具体的版本优先”。
Concepts 让模板签名更接近“接口声明”。
比如:
template<std::integral T> T function(T a, T b);看到签名就知道:这只接受整数类型。
3、Concept 的基本语法
Concept 本质上是一个编译期谓词,结果为真或假。
最基本的定义形式:
template<typename T> concept MyConcept = 某个编译期布尔表达式;例如:
template<typename T> concept Integral = std::is_integral_v<T>;但更常见的是用 requires 表达式检查“这个类型能不能做某些事”。
例如:
template<typename T> concept Addable = requires(T a, T b) { a + b; };意思是:只要 T 支持 a + b,就满足 Addable。
4、三种最常见的使用方式
约束模板参数
template<std::integral T>
T abs_diff(T a, T b) {
return a > b ? a - b : b - a;
}requires 子句
template<typename T>
requires std::integral<T>
T abs_diff(T a, T b) {
return a > b ? a - b : b - a;
}简写模板参数
std::integral auto abs_diff(std::integral auto a, std::integral auto b) {
return a > b ? a - b : b - a;
}
这三种写法语义接近。
工程里最常用的是第 1 种和第 2 种,因为可读性更稳定。
5、requires 到底是什么
requires 有两种常见角色,不要混淆。
requires 子句
放在模板声明后面,表示“这个模板启用的条件”。template<typename T>
requires std::copyable<T>
void foo(T x);requires 表达式
放在 Concept 定义里,表示“怎么检查一个类型是否满足要求”。template<typename T>
concept Printable =
requires(T x) {
std::cout << x;
};
前者是“使用约束”。
后者是“定义约束”。
6、requires 表达式的四类要求
这是 Concepts 真正的核心。
假设有:
template<typename T> concept Example = requires(T x) { typename T::value_type; { x + x }; { x + x } noexcept; { x + x } -> std::same_as<T>; };它里面可能出现四类要求。
简单要求
只要求表达式合法,不关心返回类型。x + x;
类型要求
要求某个嵌套类型存在。typename T::value_type;
复合要求
不仅要求表达式合法,还要求 noexcept、返回类型等性质。{ x + x } -> std::same_as<T>;
这里的意思是:x + x 的结果类型必须正好是 T。
嵌套要求
要求一个布尔条件成立。requires sizeof(T) > 4;
示例:
template<typename T> concept LargeAddable = requires(T a, T b) { a + b; requires sizeof(T) > 4; };7、最实用的标准库 Concepts
C++20 标准库已经提供了很多 Concepts,位于头文件 concepts 中。最常用的是这些。
same_as
两个类型完全相同std::same_as<int, int>
derived_from
是否继承自某个基类std::derived_from<Dog, Animal>
convertible_to
是否可转换std::convertible_to<int, double>
integral / floating_point
整数 / 浮点数std::integral<int>
std::floating_point<double>assignable_from
是否可赋值movable / copyable / semiregular / regular
对象语义相关约束invocable / predicate / relation
可调用对象相关totally_ordered
支持完整排序语义
例如:
template<std::totally_ordered T> const T& clamp_value(const T& x, const T& low, const T& high) { if (x < low) return low; if (high < x) return high; return x; }这比你自己手写一堆比较运算检测更清楚。
8、自定义 Concept 的典型写法
8.1 检查某个操作是否存在
template<typename T> concept HasSize = requires(T x) { x.size(); };8.2 检查返回类型
template<typename T> concept StringLike = requires(T x) { { x.data() } -> std::convertible_to<const char*>; { x.size() } -> std::convertible_to<std::size_t>; };8.3 组合已有 Concept
template<typename T> concept Numeric = std::integral<T> || std::floating_point<T>;8.4 对多个模板参数建约束
template<typename T, typename U> concept AddReturnsT = requires(T t, U u) { { t + u } -> std::same_as<T>; };9、Concepts 和 SFINAE 的关系
可以把 Concepts 理解成“更现代、更可读的 SFINAE”。
以前常见写法:
template<typename T, typename = std::enable_if_t<std::is_integral_v<T>>> T f(T x) { return x; }现在写成:
template<std::integral T> T f(T x) { return x; }优势很明显:
- 约束写在接口位置,不藏在返回类型或默认模板参数里
- 错误信息更好
- 重载排序更自然
- 代码更接近“表达意图”,而不是“欺骗编译器”
什么时候还会看到 SFINAE?
老代码、兼容 C++17、或者一些特别底层的模板技巧里仍然常见。
但如果项目是 C++20 及以上,优先用 Concepts。
10、Concepts 如何影响重载决议
这是 Concepts 的高级价值之一。
看例子:
template<typename T> void print(const T&) { std::cout << "generic\n"; } template<std::integral T> void print(const T&) { std::cout << "integral\n"; }调用:print(42);
会优先匹配受约束更严格的那个版本,也就是 integral 版本。
这背后的规则通常叫“约束更特化”或“subsumption”。
直观理解就行:
要求更具体的模板,在满足条件时优先级更高。
这让模板重载终于能像普通函数重载一样更好地表达层次。
11、一个完整示例:写一个适用于容器的打印函数
#include <concepts> #include <iostream> #include <ranges> template<typename T> concept Streamable = requires(std::ostream& os, T value) { { os << value } -> std::same_as<std::ostream&>; }; template<typename R> concept PrintableRange = std::ranges::input_range<R> && Streamable<std::ranges::range_value_t<R>>; template<PrintableRange R> void print_range(const R& range) { for (const auto& item : range) { std::cout << item << ' '; } std::cout << '\n'; }这里表达得非常清楚:
- 参数必须是一个输入区间
- 区间里的元素必须能输出到 ostream
这就是 Concepts 最强的地方:把“隐含要求”变成“显式接口”。
12、Concept 和 static_assert 有什么区别
它们都能做编译期限制,但定位不同。
Concept 更适合:
- 限制模板参与重载
- 描述模板参数接口
- 改善签名可读性
- 让错误在匹配阶段发生
static_assert 更适合:
- 在模板内部做额外约束检查
- 给出更细致的人类可读错误消息
- 检查与算法内部逻辑相关的条件
常见组合方式:
template<typename T> requires std::integral<T> T safe_div(T a, T b) { static_assert(sizeof(T) >= 4, "T must be at least 32-bit"); return a / b; }Concept 负责“入口筛选”。
static_assert 负责“内部断言”。
13、Concepts 和 ranges 经常一起出现
C++20 里,Concepts 和 Ranges 基本是配套设计。
很多 ranges 算法都带有严格的 Concept 约束。
例如你会看到类似这种签名思路:
template<std::ranges::input_range R> void algo(R&& r);这意味着:
不是“任何类型都能传”,而是“必须像一个输入范围”。
所以如果你想真正掌握现代 C++ 泛型编程,Concepts 和 Ranges 最好一起学。
14、常见误区
Concept 不是运行时机制
它完全发生在编译期,不会引入虚函数那类运行时开销。Concept 不是“类接口替代品”
它不是面向对象接口的替代,而是模板参数约束机制。检查“语法合法”不等于检查“语义正确”
比如你可以检查某个类型支持 <,但不代表它真的满足严格弱序。不要把 Concept 写得过细碎
如果一个约束只在一个函数里用一次,直接 requires 就够了。
只有当一个约束有复用价值或语义名称时,再单独提炼成 concept。不要滥用 same_as
很多时候你真正想要的是 convertible_to,而不是返回类型必须一模一样。
15、工程里的写法建议
优先使用标准库已有 Concept
比如 integral、floating_point、same_as、predicate、ranges 相关约束。自定义 Concept 时,名字表达语义,不要表达实现细节
好名字:Sortable、Hashable、Streamable
差名字:HasLessAndEqualAndCopyCtor把“通用能力”抽成 Concept,把“局部规则”留给 requires 或 static_assert
约束要尽量贴近真实需求
如果只需要能比较大小,就不要要求 copyable、default_initializable 等额外能力。公共模板接口强烈建议加约束
尤其是库代码、框架代码、基础设施代码。
16、什么时候该用 Concept
适合用的场景:
- 你在写模板库
- 你希望错误信息更可控
- 你有多个模板重载,需要明确优先级
- 你在写 ranges、容器、算法、泛型工具
- 你想替换老旧的 enable_if
不一定需要用的场景:
- 非模板代码
- 只有非常局部、一次性的模板工具
- 项目还必须兼容 C++17 或更低版本
17、一段对比:没有 Concept vs 有 Concept
没有 Concept:
template<typename T> auto sum(T a, T b) { return a + b; }问题:
你不知道 T 需要什么能力。
有 Concept:
template<typename T> concept Summable = requires(T a, T b) { a + b; }; template<Summable T> T sum(T a, T b) { return a + b; }好处:
接口意图清晰,错误更可控,维护成本更低。
18、你可以这样记忆
把 Concepts 当成模板的“编译期接口声明”。
类的成员函数签名描述“对象能做什么”。
Concept 描述“类型要满足什么,才能喂给模板”。
所以它解决的不是语法糖问题,而是泛型编程里的接口表达问题。
19、学习顺序建议
先学标准库基础 Concept
same_as、convertible_to、integral、floating_point、totally_ordered再学 requires 表达式
会写简单要求、类型要求、复合要求再学约束重载
理解“更具体约束优先”最后结合 ranges 看真实代码
这是 Concepts 最能发挥价值的地方
============================== 分割线 ==============================
如果读者对Concepts的概念还是有些模糊,可看以下部分进一步深入了解。
二、从零到一:Concepts 语法与编译器规则
1. Concepts 本质上是什么
Concept 是一个“编译期布尔条件”,用来约束模板参数。
最基本的形式:
template<class T> concept C = 条件; template<class T>concept C = 条件;例如:
#include <concepts> template<class T> concept Integral = std::integral<T>;这里的 Integral 本质上就是一个可复用的约束名。
你可以把它理解成:
类型层面的接口声明
模板参与重载前的筛选条件
编译器做模板匹配时的判定依据
2. 约束可以写在什么位置
最常见有 4 种。
写法 1:约束模板类型参数
template<std::integral T> T f(T x) { return x; }等价理解:
T 必须满足 std::integral。
写法 2:requires 子句
template<class T> requires std::integral<T> T f(T x) { return x; }适合约束比较长、或者涉及多个模板参数的情况。
写法 3:简写函数模板
std::integral auto f(std::integral auto x) { return x; }适合简单接口,但复杂模板里可读性未必最好。
写法 4:多个参数组合约束
template<class T, class U> requires std::same_as<T, U> T add(T a, U b) { return a + b; }3. 自定义 Concept 怎么写
有两种主流方式。
方式 1:基于已有 trait 或标准 concept
template<class T> concept SignedIntegral = std::integral<T> && std::is_signed_v<T>;方式 2:基于 requires 表达式检查操作
template<class T> concept Addable = requires(T a, T b) { a + b; };这表示:
只要 a + b 这个表达式对 T 合法,T 就满足 Addable。
4. requires 表达式的完整理解
requires 表达式是 Concepts 的核心。
基本形态:
template<class T> concept C = requires(T x) { 一组要求; };这里面的 T x 只是“用于检查的形参名字”,不是运行时对象。
requires 里面有 4 类要求。
4.1 简单要求
只检查表达式是否合法。
template<class T> concept Addable = requires(T a, T b) { a + b; };只要 a + b 能写,就满足。
4.2 类型要求
检查某个嵌套类型是否存在。
template<class T> concept HasValueType = requires { typename T::value_type; };4.3 复合要求
检查表达式是否合法,还能检查返回类型、异常性质。
template<class T> concept PlusReturnsT = requires(T a, T b) { { a + b } -> std::same_as<T>; };这里要求:
a + b 的结果类型必须恰好是 T。
再例如:
template<class T> concept NothrowAddable = requires(T a, T b) { { a + b } noexcept; };这里要求:
a + b 必须是 noexcept。
4.4 嵌套要求
直接要求一个编译期布尔条件成立。
template<class T> concept LargeType = requires { requires sizeof(T) >= 8; };5. 复合要求里的箭头到底是什么意思
这个很容易误解。
{ expr } -> std::same_as<int>;
意思不是“返回 int”这么简单,而是:
expr 的类型必须满足右边这个 concept。
比如:
{ a + b } -> std::convertible_to<double>;
表示:
a + b 的结果可以转换为 double。
如果写成:
{ a + b } -> std::same_as<double>;
那就严格得多,要求类型正好是 double。
工程里非常常见的坑是:
你本来只想要“能转成 bool”,结果误写成 same_as<bool>,导致大量合法类型被排除。
6. 约束检查发生在什么时候
这是 Concepts 相比老式模板最重要的点之一。
大体顺序可以这样理解:
编译器先看模板能不能作为候选
然后检查它的约束是否满足
不满足的候选会被排除
剩余候选再做重载决议
也就是说,Concepts 是“进入候选集之后、最终选择之前”的筛选机制。
这带来两个直接效果:
报错更早
重载行为更稳定
7. 为什么它比 SFINAE 好理解
SFINAE 的思路是:
“模板替换失败,不报硬错误,而是悄悄移除这个候选。”
Concepts 的思路是:
“直接告诉编译器,这个模板只对满足某些能力的类型开放。”
对比一下。
老式写法:
template<class T, class = std::enable_if_t<std::is_integral_v<T>>> T f(T x) { return x; }Concept 写法:
template<std::integral T> T f(T x) { return x; }后者的优势:
约束在接口上
错误信息更直接
不需要把约束藏进模板参数或返回类型里
更容易做约束重载
8. 编译器怎么比较“哪个约束更具体”
这就是 Concepts 的重载核心,通常叫 subsumption,可以简单理解为“约束包含关系”。
看例子:
template<class T> void g(T) { } template<std::integral T> void g(T) { }调用:g(42);
编译器会优先选 integral 版本,因为它更具体。
再看:
template<class T> requires std::integral<T> void h(T) { } template<class T> requires std::signed_integral<T> void h(T) { }调用:h(42);
如果 42 的类型是 int,那么 std::signed_integral 比 std::integral 更严格,于是第二个版本更优先。
你可以把它理解成:
“谁的适用范围更窄,但又覆盖当前实参,谁就更专用。”
9. 约束归一化与原子约束
这是偏编译器规则,但理解后能避免一些奇怪的歧义。
编译器内部不会把整个约束当成一串文本,而会把它拆成“原子约束”再比较。
例如:std::integral<T> && sizeof(T) == 4
它会拆成若干可判定条件。
为什么这重要?
因为两个看起来“语义相同”的约束,如果写法不同,不一定总能被编译器视为同样的层级。
工程上最稳的做法是:
多个重载尽量复用同一组 concept 名
不要在每个地方手写一大串近似但不完全一致的约束
把会复用的约束提炼成命名 concept
这样更利于编译器做一致的排序,也更利于人读。
10. requires 子句和 requires 表达式不要混
这两个名字一样,但角色不同。
requires 子句:使用约束
template<class T> requires std::integral<T> T f(T x);requires 表达式:定义约束
template<class T> concept C = requires(T x) { x + x; };一句话记忆:
requires 后面跟布尔条件,是“启用模板的条件”
requires 后面跟大括号,是“检查类型能力的方法”
11. 短路规则与实例化安全
约束表达式里的 && 和 || 具有短路语义。
例如:
template<class T> concept Safe = std::is_class_v<T> && requires { typename T::value_type; };如果 T 不是类类型,左边已经是 false,右边通常不会再去检查 T::value_type,从而避免不必要的问题。
这也是为什么写复杂约束时,经常先放“便宜且基础”的条件,再放更具体的检测。
12. Concept 不保证语义,只保证可检查的形式
这是非常重要的边界。
例如你可以检查:
template<class T> concept LessComparable = requires(T a, T b) { { a < b } -> std::convertible_to<bool>; };这只能说明:
T 支持 <,并且结果能转 bool。
但它不能保证:
这个 < 真正满足严格弱序,或者和 == 一致。
所以 Concepts 更像“语法与类型层面的契约”,不是数学语义证明。
13. 与 static_assert 的分工
推荐这样分工:
Concept 负责模板入口筛选
static_assert 负责模板内部的局部断言
例如:
template<std::integral T> T parse_and_scale(T x) { static_assert(sizeof(T) >= 4, "T must be at least 32 bits"); return x * 100; }这里用法很合理:
整数类型由 concept 筛掉
位宽要求由 static_assert 细化
14. Concepts 最常见的设计层级
实际项目里,建议分三层。
第一层:标准库 concept
直接用 std::integral、std::floating_point、std::same_as、std::predicate、std::ranges::input_range 这些。
第二层:领域通用 concept
比如 Streamable、Hashable、EntityLike、RepositoryLike 这类项目内可复用约束。
第三层:局部 requires
只在一个模板里用一次的规则,直接写 requires 子句,不一定要提炼命名 concept。
这样既不会过度抽象,也不会把约束写得到处都是匿名长表达式。
15. 什么时候你会遇到 Concepts 报错
典型有 3 类。
模板参数不满足约束
多个候选都满足,但约束不形成清晰的更专用关系,导致重载歧义
你在 concept 里写得太严格,排除了你本来想支持的类型
所以调试时优先检查:
我真正想要的是 same_as,还是 convertible_to
我要求的是表达式存在,还是返回值精确类型
这个约束是模板入口约束,还是算法内部规则
三、10 个高质量示例:实际写法与陷阱
示例 1:只接受整数
#include <concepts> template<std::integral T> T gcd(T a, T b) { while (b != 0) { T t = a % b; a = b; b = t; } return a; }适用场景:
数值算法、位运算、计数器逻辑。
关键点:
签名直接表达“这是整数算法”。
示例 2:接受整数或浮点数
template<class T> concept Numeric = std::integral<T> || std::floating_point<T>; template<Numeric T> T square(T x) { return x * x; }关键点:
组合 concept 比反复写长 requires 更清晰。
常见坑:
不要把 Numeric 写得太宽,比如把所有支持乘法的类型都塞进去,最后语义会变得很模糊。
示例 3:检查流输出能力
#include <concepts> #include <iostream> template<class T> concept Streamable = requires(std::ostream& os, const T& value) { { os << value } -> std::same_as<std::ostream&>; }; template<Streamable T> void print_one(const T& value) { std::cout << value << '\n'; }常见坑:
很多人会写成只检查 os << value; 合法,但不检查返回值。多数情况下没问题,但若你要和标准流式接口保持一致,检查返回 std::ostream& 更稳。
示例 4:检查容器是否有 size
#include <concepts> template<class T> concept HasSize = requires(const T& x) { { x.size() } -> std::convertible_to<std::size_t>; }; template<HasSize T> bool is_empty_like(const T& x) { return x.size() == 0; }常见坑:
如果你写成 std::same_asstd::size_t,会过严。因为很多 size 的返回类型并不一定恰好就是 std::size_t,但通常都可转换。
示例 5:要求加法结果还是自身类型
template<class T> concept ClosedAddable = requires(T a, T b) { { a + b } -> std::same_as<T>; }; template<ClosedAddable T> T add_twice(T a, T b) { return a + b + b; }这类约束表达的是“封闭运算”。
适用场景:
向量、数值类型、矩阵类。
常见坑:
如果 T 是代理类型或者表达式模板类型,这个约束可能太死。很多现代库里 a + b 返回的是中间表达式类型,不一定是 T。
示例 6:用 concept 做重载分发
#include <iostream> template<class T> void describe(const T&) { std::cout << "generic\n"; } template<std::integral T> void describe(const T&) { std::cout << "integral\n"; } template<std::floating_point T> void describe(const T&) { std::cout << "floating\n"; }这比 tag dispatch 或 enable_if 可读性高很多。
关键点:
约束越具体,重载越自然。
示例 7:结合 ranges 约束可迭代区间
#include <concepts> #include <iostream> #include <ranges> template<class T> concept Streamable = requires(std::ostream& os, const T& value) { { os << value } -> std::same_as<std::ostream&>; }; template<class R> concept PrintableRange = std::ranges::input_range<R> && Streamable<std::ranges::range_reference_t<R>>; template<PrintableRange R> void print_range(R&& range) { for (auto&& x : range) { std::cout << x << ' '; } std::cout << '\n'; }常见坑:
很多人检查的是 range_value_t<R>,但某些区间的引用类型和 value 类型不同。打印时更贴近实际的是 range_reference_t<R>。
示例 8:约束可调用对象
#include <concepts> #include <functional> template<class F, class T> concept UnaryTransformer = std::regular_invocable<F, T> && requires(F f, T x) { f(x); }; template<class F, class T> requires UnaryTransformer<F, T> auto apply_once(F f, T x) { return f(x); }适用场景:
回调、策略函数、算法定制点。
常见坑:
只检查 f(x) 能不能调用,忘了检查 const 性、返回值类型、异常要求。
示例 9:多参数约束
template<class T, class U> concept AddableTo = requires(T t, U u) { t + u; }; template<class T, class U> requires AddableTo<T, U> auto add(T t, U u) { return t + u; }适用场景:
混合数值、字符串拼接、异构表达式。
常见坑:
如果你真正依赖的是返回结果还能继续参与某种运算,就应该继续约束返回类型,而不是只检查 t + u 存在。
示例 10:从错误的 concept 到正确的 concept
错误写法:
template<class T> concept BadStringLike = requires(T x) { { x.data() } -> std::same_as<const char*>; { x.size() } -> std::same_as<std::size_t>; };问题:
这个约束太严格,很多本来“像字符串”的类型都会被排除。
更合理的写法:
template<class T> concept StringLike = requires(T x) { { x.data() } -> std::convertible_to<const char*>; { x.size() } -> std::convertible_to<std::size_t>; };这就是 Concepts 最常见的工程坑:
写成“精确类型匹配”,但真实需求只是“可用”。
四、最常见的 8 个坑
1. 把 same_as 用滥了
如果你只是要“能当成 bool 用”,写:std::convertible_to<bool>
而不是:std::same_as<bool>
2. 只检查语法,不检查你真正依赖的性质
你模板里如果后面要保存返回值、继续链式调用、要求不抛异常,就不要只写一个简单要求。
3. concept 名字写成实现细节堆砌
差名字:
- HasBeginEndAndDereferenceableIteratorAndComparableValue
- SupportsPlusMinusMulDivAndAssign
好名字:
- RangeLike
- NumericLike
- Streamable
名字应该表达语义,不是把检测细节全抄到名字里。
4. 明明只局部使用,却过度抽象成公共 concept
如果一个约束只在一个函数里出现一次,而且业务语义不稳定,直接写 requires 子句通常更合适。
5. 约束写得过宽
例如:concept Printable = requires(T x) { std::cout << x; };
如果项目里你真正需要的是“稳定流式输出接口”,这个约束可能太松了。
6. 约束写得过严
例如强制 size 返回 std::size_t,或者 data 必须返回 const char*,都会无意中排掉很多合法类型。
7. 多个重载约束相近但不一致,导致歧义
例如两个重载分别手写不同的长 requires,语义接近但编译器无法判断谁更专用。解决方法通常是:
抽取公共 concept
让专用版本明确在通用版本之上增强约束
8. 用 concept 试图表达无法在编译期可靠验证的语义
例如“是否是严格弱序比较器”“是否线程安全”“是否性能足够好”,这些不是 concept 擅长表达的东西。
五、实战写法建议
如果你在工程里开始用 Concepts,建议按这个顺序落地。
先把 enable_if 最多的公共模板替换成标准 concept
优先替换接口层,而不是一上来重写所有模板细节
先用标准库 concept,再提炼少量项目级 concept
对 ranges、回调、算法模板最值得优先引入
对局部规则,优先 requires 子句,而不是新增一堆 concept 名称
一条很实用的判断标准:
如果一个约束名字能明显提升接口可读性,就值得抽成 concept。
如果抽出来反而让人不知道你在检查什么,就直接写 requires。
六、一套很实用的记忆框架
可以把 Concepts 记成 4 句话:
concept 是模板参数的编译期接口
requires 表达式是“怎么检查接口”
requires 子句是“什么时候启用模板”
重载时,约束更具体的模板优先
如果你把这 4 句彻底吃透,Concepts 的大框架就已经稳了。
七、学习下一步
如果你想继续深入,最值得接着学的是这 3 块:
Concepts 与 ranges 的配合,尤其是 input_range、forward_range、view
约束重载与 subsumption 的边界案例
如何把老代码里的 enable_if 和 detection idiom 平滑迁移到 Concepts
