第22章 约束与要求:requires表达式、requires子句、复合约束与嵌套约束
C++20 的 Concepts,说白了就是给模板参数加「门槛」。
我早年写模板时,最头疼的就是编译错误。动辄几百行的报错信息,你根本不知道是哪个类型没满足要求。后来 Concepts 出来了,嗯,这玩意儿确实救了不少命。
今天咱们就聊聊约束与要求的具体写法。requires 表达式、requires 子句、复合约束、嵌套约束——这些名字听着唬人,其实拆开看,每个都不复杂。
22.1 约束的基本概念
约束(Constraint)是对模板参数的一组要求。说白了,就是告诉编译器:「嘿,只有满足这些条件的类型,才能用这个模板」。
我个人习惯把约束分成两类:
- 语法约束:类型必须支持某种操作,比如有
+运算符 - 语义约束:类型必须满足某种语义,比如可拷贝、可比较
在 C++20 之前,我们只能用 SFINAE 和 static_assert 来模拟约束。说实话,那体验真不怎么样。我在项目中曾经写过一段极其复杂的 enable_if 链,后来自己都看不懂了。
核心要点:约束是编译期检查,不会影响运行时性能。你尽管用,编译器会帮你搞定一切。
22.2 requires 子句
requires 子句是给模板或函数加约束的最直接方式。它放在模板参数列表后面,函数声明前面。
template<typename T>
requires std::integral<T>
T add(T a, T b) {
return a + b;
}
这段代码的意思是:只有 T 是整数类型时,add 才参与重载决议。
你想想看,如果传个 std::string 进来,编译器直接说「不匹配」,而不是报一堆看不懂的内部错误。多清爽。
requires 子句也可以写在函数声明后面:
template<typename T>
T add(T a, T b) requires std::integral<T> {
return a + b;
}
两种写法等价。我个人偏好第一种,因为约束和模板参数挨得近,读起来更直观。
22.3 requires 表达式
requires 表达式是约束的「原子单位」。它用来检查某个类型是否支持特定的操作。
语法长这样:
template<typename T>
concept HasPlus = requires(T a, T b) {
a + b; // 要求 T 支持加法
};
这里 requires(T a, T b) { a + b; } 就是一个 requires 表达式。它检查:给定两个 T 类型的变量,能否执行 a + b。
requires 表达式可以包含多条要求:
template<typename T>
concept Comparable = requires(T a, T b) {
a == b;
a != b;
a < b;
};
我在项目中遇到过这样的情况:有个泛型排序算法,要求类型必须支持 < 和 ==。用 requires 表达式写个 concept,清晰又安全。
小技巧:requires 表达式里的参数只是「占位符」,不需要实际定义变量。编译器只检查表达式是否合法,不会真的执行。
22.4 复合约束
复合约束(Compound Requirement)是 requires 表达式的进阶用法。它不光检查表达式是否合法,还能检查返回类型。
语法:
template<typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>;
};
这段代码的意思是:a + b 必须合法,而且结果必须能转换成 T 类型。
复合约束的格式是:{ 表达式 } -> 类型约束;
常见的类型约束有:
std::same_as<T>:结果类型必须完全等于 Tstd::convertible_to<T>:结果类型可以隐式转换成 Tstd::derived_from<T>:结果类型必须派生自 T
我曾经踩过一个坑:写复合约束时忘了加 std::,结果编译器报了一堆看不懂的错误。嗯,这里要注意,约束里的类型必须完整限定。
22.5 嵌套约束
嵌套约束(Nested Requirement)就是在 requires 表达式里再套一个 requires 表达式。听起来有点绕,其实很实用。
template<typename T>
concept Container = requires(T c) {
typename T::value_type; // 必须有 value_type
typename T::iterator; // 必须有 iterator
{ c.begin() } -> std::same_as<typename T::iterator>;
{ c.end() } -> std::same_as<typename T::iterator>;
requires std::regular<T>; // 嵌套约束:T 必须满足 regular 概念
};
注意最后一行:requires std::regular<T>; 这就是嵌套约束。它要求 T 必须同时满足 std::regular 概念。
嵌套约束的好处是:你可以把复杂的约束拆成小块,然后组合起来。就像搭积木一样。
注意:嵌套约束里的 requires 关键字不能省略。少了它,编译器会认为你在声明一个变量,而不是约束。
22.6 约束的组合
约束可以用逻辑运算符组合:
&&:与关系,两个约束都必须满足||:或关系,满足一个即可!:非关系,取反
template<typename T>
requires std::integral<T> && (sizeof(T) >= 4)
T safe_add(T a, T b) {
return a + b;
}
这段代码要求 T 是整数类型,而且大小至少 4 字节。说白了,就是只允许 int、long 等类型,不允许 char 或 short。
约束组合时,优先级和普通逻辑运算一样:! 最高,&& 次之,|| 最低。拿不准就加括号,别省。
22.7 知识体系图
下面这张图展示了约束与要求的整体结构:
22.8 实际项目中的经验
我在一个大型图形引擎项目里用过 Concepts。当时有个模板函数,需要处理各种数学向量——二维、三维、四维,每种都有不同的精度(float、double)。
如果用 SFINAE,代码会变成一团乱麻。但用 Concepts 之后,每个向量类型定义一个 concept,然后组合使用,代码清晰多了。
template<typename T>
concept Vec2 = requires(T v) {
{ v.x } -> std::floating_point;
{ v.y } -> std::floating_point;
};
template<typename T>
concept Vec3 = requires(T v) {
{ v.x } -> std::floating_point;
{ v.y } -> std::floating_point;
{ v.z } -> std::floating_point;
};
template<Vec3 V>
V cross_product(const V& a, const V& b) {
// 三维叉积实现
}
你看,cross_product 只接受三维向量。如果传个二维向量进来,编译器直接报错,而不是在模板实例化时炸出一堆内部错误。
避坑指南:我曾经在 requires 表达式里忘了写分号,结果编译器报错说「expected ';'」。嗯,requires 表达式里的每条要求都要加分号,和普通语句一样。
22.9 约束的偏序关系
约束之间是有偏序关系的。说白了,就是「谁更严格」的问题。
比如 std::integral<T> 比 std::signed_integral<T> 更宽松。因为所有有符号整数都是整数,但整数不一定是带符号的。
编译器在重载决议时,会选择约束更严格的版本。这叫做「约束的偏序」。
template<typename T>
requires std::integral<T>
void process(T v) {
// 通用整数处理
}
template<typename T>
requires std::signed_integral<T>
void process(T v) {
// 有符号整数专用处理
}
如果传 int 进来,编译器会选择第二个版本,因为它更严格。传 unsigned int 进来,则选第一个。
这个机制非常有用。你可以写一个通用版本,再写几个特化版本,编译器会自动选择最合适的。
22.10 总结
约束与要求是 C++20 模板编程的核心升级。它让模板代码更安全、更易读、更易维护。
我个人觉得,Concepts 最大的价值不是「能做什么」,而是「能避免什么」。它避免了那些让人抓狂的编译错误,避免了 SFINAE 的复杂写法,避免了模板代码的「黑魔法」感。
如果你还在用 C++17 或更早的标准,我建议你尽快升级到 C++20。哪怕只为了 Concepts,也值得。
一句话总结:requires 子句是门槛,requires 表达式是检查工具,复合约束检查返回值,嵌套约束组合条件。四者配合,让模板编程变得优雅而安全。
公众号:蓝海资料掘金营,微信 deep3321