第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>:结果类型必须完全等于 T
  • std::convertible_to<T>:结果类型可以隐式转换成 T
  • std::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 字节。说白了,就是只允许 intlong 等类型,不允许 charshort

约束组合时,优先级和普通逻辑运算一样:! 最高,&& 次之,|| 最低。拿不准就加括号,别省。

22.7 知识体系图

下面这张图展示了约束与要求的整体结构:

约束与要求知识体系 约束与要求 requires 子句 requires 表达式 复合约束 放在模板参数后 或函数声明后 检查表达式合法性 可定义 concept 检查返回类型 格式:{ expr } -> 约束 嵌套约束 requires 中的 requires 组合:&& || !

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