concepts是c++20引入的编译期模板参数语义约束机制,非运行时检查或接口定义;它通过requires子句或命名concept对类型施加操作可行性约束,提升错误信息可读性,零开销且仅限c++20及更新编译器支持。

Concepts 是什么,不是什么
Concepts 不是运行时检查,也不是接口定义;它是编译期对模板参数的语义约束。你写 std::sortable,编译器不会去跑排序算法验证,而是检查类型是否提供必需的操作(比如 operator、可迭代、可交换等)。它替代的是过去靠 SFINAE 或 <code>static_assert 堆出来的模糊报错,让错误信息从“no type named 'iterator' in 'int'”变成“int does not satisfy sortable”。
常见错误现象:把 Concepts 当成类型别名或运行时断言用;或者以为加了 requires 就能自动推导类型——其实它不参与重载解析的优先级排序,只做硬性过滤。
- 使用场景:泛型容器、算法库、自定义 trait 约束(如
addable<t u></t>) - 性能影响:零开销——所有检查在编译期完成,生成代码与手动
static_assert无异 - 兼容性:仅 C++20 起支持,GCC 10+、Clang 12+、MSVC 19.29+;C++17 项目不能混用
怎么写一个最小可用的 Concept
最简形式就是用 concept 关键字 + 布尔常量表达式。别急着套 std::ranges 那套,先从判断有没有某个成员函数开始:
template<typename T>
concept has_size = requires(T t) {
t.size();
};
注意这里 requires 块里写的是“能调用”,不是“返回 int”或“是 const 成员”——那是更细粒度的约束,要额外加条件。
立即学习“C++免费学习笔记(深入)”;
- 容易踩的坑:
requires(T t)中的t是纯占位符,不构造对象;但若类型没有默认构造函数,就得改用requires(std::declval<t>().size())</t> - 参数差异:单参数 concept 直接跟在模板参数后;多参数(如
addable<t u></t>)需显式列出并检查T + U是否合法 - 别写
requires std::is_integral_v<t></t>这种——这是 type trait,不是 concept;要用就封装成integralconcept
在函数模板中用 requires 还是 concept 名字
两者都能用,但语义不同:requires 是内联约束,适合一次性的简单条件;命名 concept 更利于复用和错误提示。
// 推荐:清晰、可读、报错直接显示概念名
template<has_size T>
void print_size(T&& c) { std::cout << c.size() << '\n'; }
// 可用但不推荐:约束逻辑散落在函数声明里,难复用
template<typename T>
requires requires(T t) { t.size(); }
void print_size(T&& c) { std::cout << c.size() << '\n'; }
- 使用场景:库作者应优先定义命名 concept;临时脚本或调试时可用
requires快速验证 - 错误信息差异:前者报错含
has_size,后者只说“requires clause not satisfied”,调试成本高 - 注意:不能在非模板上下文中用 concept(比如普通函数参数),会编译失败
和 SFINAE、enable_if 比较时的真实取舍
Concepts 不是 SFINAE 的升级版,而是替代方案——它解决的是同一问题的不同侧面。SFINAE 本质是“让错误不发生”,Concepts 是“让错误说得清”。
- 如果你的模板需要精细控制重载顺序(比如优先匹配
std::vector而非任意Container),SFINAE 仍不可替代;Concepts 只做准入,不做排序 - 如果你的约束逻辑跨多个模板参数(如
T::value_type == U::key_type),用 Concepts 写起来更直白,不用嵌套decltype和std::declval - 兼容旧代码时,别强行把
std::enable_if_t全换成 concept——尤其当模板已被广泛特化时,约束变更可能破坏 ABI 或导致重载歧义
最常被忽略的一点:Concepts 对模板参数推导本身没帮助。写 template<has_size t> void f(T)</has_size>,传入 std::string{} 没问题;但传入 42,编译器不会因为 has_size 失败就尝试其他重载——它直接报错,不回退。









