概念与约束(C++20):concept的定义,requires子句,约束的排序与组合
说实话,C++模板写多了,最头疼的是什么?是编译错误。那几百行的报错信息,简直能把人看哭。C++20 引入的 concept,说白了就是给模板参数加了一道「安检门」——类型不对,直接告诉你哪里不对,而不是等到实例化时炸出一堆天书。
我个人习惯把 concept 理解为「类型的能力清单」。你要求这个类型能加、能减、能拷贝,那就写个 concept 列清楚。编译器在调用点就帮你检查,而不是等到模板内部展开才报错。
concept 的定义
先看一个最简单的例子。我想定义一个 concept,要求类型 T 可以相加:
template<typename T>
concept Addable = requires(T a, T b) {
a + b;
};
这里 requires 后面跟的是一个表达式约束。它检查 a + b 这个表达式是否合法。合法就通过,不合法就报错。
我在项目中遇到过一个问题:有人把 concept 写成了运行时检查。注意,concept 是编译期求值的,它不会生成任何运行时代码。你写 if constexpr (Addable<T>) 没问题,但别想着在运行时动态判断。
核心要点:concept 是一个编译期谓词,返回 true 或 false。它只检查语法合法性,不检查语义正确性。
再复杂一点,我们可以组合多个要求:
template<typename T>
concept Comparable = requires(T a, T b) {
a == b;
a != b;
{ a < b } -> std::convertible_to<bool>;
};
注意最后一行,{ a < b } -> std::convertible_to<bool> 表示:表达式 a < b 的结果必须能隐式转换为 bool。这种写法比单纯写 a < b 更严格,也更安全。
requires 子句的多种用法
requires 子句其实有四种形式,我列个表方便你对照:
| 形式 | 示例 | 说明 |
|---|---|---|
| 简单要求 | a + b; |
表达式合法即可 |
| 类型要求 | typename T::value_type; |
类型存在即可 |
| 复合要求 | { expr } noexcept -> concept; |
检查表达式类型和异常说明 |
| 嵌套要求 | requires Addable<T>; |
在 requires 内部再套 concept |
嗯,这里要注意:类型要求经常被忽略。比如你想检查一个容器是否有 iterator 类型:
template<typename T>
concept HasIterator = requires {
typename T::iterator;
};
我曾经在重构旧代码时,发现有人用 SFINAE 写了十几行才能做到的事,concept 三行就搞定了。这就是差距。
约束的排序与组合
约束可以组合,也可以排序。组合用 && 和 ||:
template<typename T>
concept Sortable = std::ranges::random_access_range<T> && Comparable<std::ranges::range_value_t<T>>;
排序呢?说白了就是「哪个约束更特殊,哪个优先级更高」。C++20 有一套约束偏序规则:
Addable && Comparable比单独的Addable更特殊- 如果两个模板都能匹配,编译器选更特殊的那一个
- 如果无法判断谁更特殊,编译报歧义错误
举个例子:
template<Addable T>
void process(T v) {
std::cout << "addable\n";
}
template<Addable T> requires Comparable<T>
void process(T v) {
std::cout << "addable and comparable\n";
}
如果你传一个 int,它会调用第二个版本。因为 int 既满足 Addable 又满足 Comparable,而第二个约束更严格。编译器会自动选择约束更强的那个。
小技巧:写模板重载时,尽量用 concept 代替 enable_if。代码可读性提升不止一个档次。我团队里现在禁止在新代码中使用 enable_if,除非有特殊理由。
约束的原子性与合取/析取
每个 concept 定义本身就是一个「原子约束」。当你用 && 组合时,形成合取约束;用 || 组合时,形成析取约束。
这里有个坑:
template<typename T>
concept A = true;
template<typename T>
concept B = true;
template<A T> void foo(T); // (1)
template<B T> void foo(T); // (2)
这两个模板的约束是等价的吗?不是。虽然 A 和 B 都求值为 true,但它们是不同的原子约束。所以 (1) 和 (2) 是歧义的——编译器不知道选哪个。
我曾经在代码评审中看到有人这么写,然后抱怨「为什么编译不过」。原因就在这里:原子约束的身份由 concept 名称和模板参数决定,而不是由求值结果决定。
知识体系图
下面这张图总结了本章的核心逻辑:
实际项目中的避坑指南
最后分享几个我踩过的坑:
警告:concept 不能递归定义自己。比如 concept C = requires(T t) { requires C<T>; }; 是无限递归,编译器会报错。
我曾经在写一个序列化库时,想用 concept 检查类型是否可序列化。结果不小心写了递归引用,编译直接卡死。排查了半天才发现问题。
另一个常见问题是:concept 中的 requires 表达式不会实例化模板。也就是说,如果你写 requires { std::vector<T>{}; },它不会真的构造一个 vector,只是检查语法是否合法。这跟 decltype 的行为类似。
嗯,concept 这东西,用好了是神器,用不好就是坑。建议你从简单的开始,先定义几个基础的 concept,比如 Addable、Comparable,慢慢再组合成复杂的。别一上来就想搞个万能 concept,那反而容易把自己绕进去。
公众号:蓝海资料掘金营,微信deep3321