concepts 与 requires¶
| 标准 | 难度 | 状态 | 分级 | 证据 | 审阅 | 最后校验 |
|---|---|---|---|---|---|---|
| C++20 | 4/5 | final | A | 3 条(manual 3) | 单审(zyuhao7) | 2026-10-24 |
编译器支持:GCC 10+ · Clang 10+ · MSVC 19.30+。版本查询:cppreference 编译器支持。
主张与证据¶
| 主张 | 支持证据 |
|---|---|
| 用 concept 包装已有的 is_formattable 特征 | ev-fmt-concepts-formattable-001 |
| 用 requires 限制标量相等比较重载 | ev-json-concepts-scalar-eq-002 |
| 同一标量约束复用于三路比较重载 | ev-json-concepts-scalar-spaceship-003 |
本站证据页:ev-fmt-concepts-formattable-001 · ev-json-concepts-scalar-eq-002 · ev-json-concepts-scalar-spaceship-003
解决什么问题¶
C++20 之前,模板对参数的要求只写在文档和错误信息里。给 std::sort 传一个不可比较的类型,编译器会在实例化深处抛出几十行错误;想限制重载候选,只能用 std::enable_if 把条件塞进返回类型或模板参数,签名迅速膨胀,错误信息仍然指向被剔除的候选而不是真正原因。
concepts 把“类型需要满足什么”写成具名约束,放在模板参数列表或函数声明上。约束参与重载决议,失败发生在候选筛选阶段;不满足时编译器能指出哪条约束失败,而不是从模板体内部报错。
基础用法¶
#include <concepts>
#include <iostream>
template <typename T>
concept Incrementable = requires(T value) {
{ ++value } -> std::same_as<T&>;
value++;
};
template <Incrementable T>
T advance_one(T value) {
++value;
return value;
}
int main() {
std::cout << advance_one(41) << '\n';
}
完整示例见 examples/cpp20/concepts.cpp:
requires(T value) { ... } 不求值,只检查表达式是否合法、返回类型是否满足要求。Incrementable 作为约束写在模板参数上,不满足的类型不会进入函数体实例化。示例另外用 Numeric 约束普通函数,用 requires 子句约束范围算法。
进阶用法¶
四类 requirement。 简单要求写表达式(value.size());类型要求写 typename T::value_type;复合要求写 { expr } -> Concept,同时检查表达式合法与返回类型;嵌套要求写 requires OtherConcept<T>,用于组合其他约束。复合要求里的 noexcept 也可以检查。
约束可以组合并参与偏序。 requires std::integral<T> && std::totally_ordered<T> 是两个原子约束的合取;多个重载的约束满足子集关系时,更受约束的版本优先。std::same_as<T, int> 与 std::integral<T> 不是同一原子约束,不会互相替代。
缩写函数模板更短。 void f(std::integral auto x) 等价于 template <std::integral T> void f(T x)。多个 auto 参数各自引入模板参数;需要引用同一类型时仍写显式模板参数。
concept 只检查声明,不检查函数体。 约束满足不代表调用一定能编译:函数体里的操作仍可能失败。concept 也无法表达“语义正确”,例如“比较必须传递”——这些是文档约定,不是类型系统能完全验证的。
requires 表达式可以做编译期探测。 它可以脱离 concept 单独使用,例如 static_assert(requires(T t) { t.begin(); });。但在非模板上下文中,表达式里的硬错误不会自动变成 false;需要依赖模板参数才会触发替换失败语义。
concept 不能被特化。 需要细化约束时,定义新的 concept 并在定义中引用旧的:template<class T> concept Sized = std::ranges::range<T> && requires(T& t) { std::size(t); };。不要试图给已有 concept 写特化。
requires 子句与 requires 表达式是两件事。 前者是写在声明上的约束(template<class T> requires C<T>),后者是产生布尔值的表达式(requires(T t) { ... })。requires requires 是合法的,但通常拆成具名 concept 更易读。
约束与返回类型错误是两类诊断。 约束在候选筛选阶段检查;返回类型或函数体内部的错误发生在约束满足之后。不要把返回类型里的硬错误误认为约束失败,也不要指望约束能替代对函数体的完整检查。
真实项目证据¶
| 项目 | 位置 | 用法 | 为什么典型 |
|---|---|---|---|
| fmtlib/fmt | include/fmt/base.h#L2738-L2741 |
用 concept 包装已有的 is_formattable 特征 |
在保留旧 trait 的同时给 C++20 用户提供约束接口,迁移成本低 |
| nlohmann/json | include/nlohmann/json.hpp#L3723-L3730 |
用 requires std::is_scalar_v<ScalarType> 限制标量比较重载 |
约束写在声明处,避免标量重载与容器重载产生歧义 |
| nlohmann/json | include/nlohmann/json.hpp#L3756-L3763 |
同一约束用于三路比较重载 | 多个运算符共享一致的标量约束,减少重复的条件编译 |
三条证据均为 manual 级:作者复核,尚未引入第二名审核者。
陷阱与边界¶
- concept 不保证语义正确。 满足语法要求不代表满足数学性质;相等、排序、哈希的一致性仍需作者保证。
- 诊断指向失败的原子约束。 把最容易失败、最能说明问题的约束放前面,通常能得到更短的信息;不要依赖短路求值来组织编译期逻辑。
- 不要在 concept 里做重计算。 每个候选、每次重载决议都可能实例化约束;复杂元编程会显著拖慢编译。
requires表达式不求值,但有副作用类型检查。 表达式中的类型必须可解析;未定义的类型名会导致硬错误,而不是false。- concept 不能替代
if constexpr。 concept 控制“是否接受这个类型”,if constexpr控制“在函数体内走哪条实现”。两者经常一起用,但职责不同。 - 缩写模板的
auto可能产生意外重载。void f(Concept auto)与void f(T)的候选集不同;重载决议时确认约束确实参与了筛选。 - 不要用 concept 约束虚函数。 虚函数不能是模板,约束也无法按动态类型选择;需要运行期多态时使用继承或类型擦除。
- concept 名称与变量模板同名会冲突。 同一命名空间里不要同时定义
concept X和template<class> bool X。
库联动¶
- fmt:
formattableconcept 建立在is_formattabletrait 上,C++20 用户可以用它约束自己的格式化包装,而不必依赖内部 trait;C++17 用户仍走旧路径。 - nlohmann/json:标量比较重载用
requires限制ScalarType,避免与数组、对象重载竞争;三路比较复用同一约束,保证==与<=>的候选集一致。 - ranges:
std::ranges::range、std::ranges::view、std::ranges::borrowed_range本身就是 concept,容器适配器与视图通过它们约束接口。自定义范围类型应满足这些 concept,而不是只提供begin/end成员。 - 与 SFINAE 的分工:新代码优先用 concept 表达接口约束;需要兼容 C++17 或需要更细粒度的替换失败控制时,保留
enable_if。两者可以共存,但同一接口不要混用两套约束风格。
横向对比¶
| 方案 | 约束位置 | 参与重载决议 | 诊断质量 | 兼容性 |
|---|---|---|---|---|
concepts / requires |
模板参数或声明 | 是 | 最好,指出失败约束 | C++20 起 |
enable_if |
返回类型或模板参数 | 是 | 差,候选被静默剔除 | C++11 起 |
static_assert |
函数体 | 否 | 中等,但已进入实例化 | C++11 起 |
| tag dispatch | 重载参数 | 是 | 中等,需要额外类型 | C++03 起 |
| 文档约定 | 无 | 否 | 无 | 任意 |
跨标准演进:C++20 引入 concepts 与 requires 表达式;C++23 未改变核心规则;C++26 的扩展主要围绕标准库 concept 的补充,不改变用户定义与重载决议机制。