requires 表达式¶
| 标准 | 难度 | 状态 | 分级 | 证据 | 审阅 | 最后校验 |
|---|---|---|---|---|---|---|
| C++20 | 3/5 | final | A | 3 条(manual 3) | 单审(zyuhao7) | 2026-10-26 |
编译器支持:GCC 10+ · Clang 10+ · MSVC 19.30+。版本查询:cppreference 编译器支持。
主张与证据¶
| 主张 | 支持证据 |
|---|---|
| 用 requires 表达式检测成员是否存在,并在 if constexpr 中据此选择分支 | ev-mold-requires-expression-unit-type-001 |
| 用 requires 表达式把静态成员存在性封装成可复用的 concept | ev-mold-requires-expression-concepts-002 |
| 解构器测试覆盖 lambda 签名中 requires 表达式的修饰名 | ev-abseil-requires-expression-mangling-003 |
本站证据页:ev-abseil-requires-expression-mangling-003 · ev-mold-requires-expression-concepts-002 · ev-mold-requires-expression-unit-type-001
解决什么问题¶
C++17 之前,探测“某个类型是否支持某个操作”通常要写 SFINAE:std::void_t、decltype、enable_if 和重载一起使用。表达式能编译就选中某个重载,不能编译就替换失败。这种写法能工作,但嵌套深、错误信息长,而且约束的意图藏在类型系统里。
requires 表达式把“这些表达式是否合法”直接写成一个布尔值。它不执行表达式,只检查语法和语义有效性,可以放在 static_assert、if constexpr 或概念定义中使用。
基础用法¶
#include <concepts>
#include <cstddef>
#include <iostream>
template <typename T>
concept HasValue = requires(T t) {
t.value;
};
template <typename T>
concept Sized = requires(const T &t) {
typename T::value_type;
{ t.size() } -> std::convertible_to<std::size_t>;
};
struct Entry {
using value_type = int;
int value;
std::size_t size() const { return 1; }
};
struct Empty {};
int main() {
std::cout << HasValue<Entry> << ' ' << HasValue<Empty> << ' '
<< Sized<Entry> << ' ' << Sized<Empty> << '\n';
}
requires(T t) { ... } 的参数是假设变量,只用于书写检查表达式,不会真的构造对象。t.value; 是简单要求,只检查成员访问是否合法;typename T::value_type; 是类型要求;{ t.size() } -> std::convertible_to<std::size_t>; 是复合要求,同时检查表达式合法且返回类型可转换。
进阶用法¶
requires 表达式通常包装成 concept,再用于约束重载。复合要求可以同时检查表达式合法、返回类型可转换,以及是否 noexcept。
template <typename T>
concept Sized = requires(const T &value) {
typename T::value_type;
{ value.size() } -> std::convertible_to<std::size_t>;
};
template <Sized T>
std::size_t length(const T &value) {
return value.size();
}
template <typename T>
std::size_t length(const T &) {
return 0;
}
Sized<T> 为真时,第一个重载比无约束版本更特殊,重载选择会优先选它;不满足时退回到返回 0 的版本。整个过程只检查表达式能否编译,不会调用 value.size(),因此 requires 表达式不能替代运行期前置条件检查。
完整示例见 examples/cpp20/requires_expressions.cpp。
真实项目证据¶
| 项目 | 位置 | 用法 | 为什么典型 |
|---|---|---|---|
| mold | src/gdb-index.cc#L188-L192 |
用 requires { hdr.unit_type; } 判断 DWARF 头是否有该字段 |
不同 DWARF 版本字段不同,条件表达式让旧格式仍能参与编译 |
| mold | src/elf.h#L1796-L1798 |
用 requires { E::R_IRELATIVE; } 等定义架构能力概念 |
把“某架构是否提供某个静态成员”封装成概念,后续约束直接复用 |
| abseil-cpp | absl/debugging/internal/demangle_test.cc#L295-L300 |
用测试覆盖 lambda 签名中 requires 表达式的修饰名 | requires 表达式进入符号修饰规则,解构器必须能处理嵌套检查 |
三条证据均为 manual 级:作者复核,尚未引入第二名审核者。前两条来自 mold 生产路径,第三条是 Abseil 解构器测试,覆盖 requires 表达式在修饰名中的表示。
陷阱与边界¶
requires 表达式不是 requires 子句。 requires { ... } 求值为 bool,是一个表达式;template <typename T> requires C<T> 是约束子句。两者可以组合,但语法位置和作用不同。
它检查“能否编译”,不检查语义是否正确。 requires(T t) { t.size(); } 只要求 size() 可调用;返回类型不对、返回垃圾值、运行期抛异常都不影响结果。需要约束返回类型时,写成复合要求 { t.size() } -> std::same_as<std::size_t>;。
表达式不会被求值。 requires(T t) { t.pop(); } 不会真的调用 pop(),也不会检查容器是否为空。依赖运行期状态的前置条件,requires 表达式表达不了。
复合要求可以检查 noexcept。 { expr } noexcept -> Concept; 同时要求表达式不抛异常并满足返回类型约束。只写 { expr } -> Concept; 不会检查异常规格。
类型要求和表达式要求分开写。 typename T::value_type; 检查嵌套类型是否存在;去掉 typename 后含义会变成表达式检查。两者可以组合,但不要把它们当成同一种要求。
requires 表达式不能吞掉所有错误。 它把紧邻的无效表达式变成“不满足”,但模板实例化深处的硬错误仍可能直接报错。约束应尽量写在接口层,而不是依赖 requires 捕获所有实现细节。
概念约束按从左到右的合取求值。 C<T> && requires { ... } 中,左侧不满足时右侧不会继续检查。这个顺序可以用来先排除明显不合适的类型,但要避免把副作用写进约束。
库联动¶
- mold:用简单要求检测 DWARF 头字段,并把 ELF 架构是否提供重定位静态成员封装成可复用概念。
- Abseil:解构器测试覆盖 requires 表达式进入函数签名修饰名后的符号,保证调试输出能还原 lambda 签名。
- 与 requires 子句的分工:requires 表达式求值为
bool,可以放在static_assert、if constexpr或概念定义中;requires 子句用于约束模板或函数,两者可以组合,但语法位置不同。
横向对比¶
| 方案 | 表达位置 | 错误信息 | 可复用性 | 主要代价 |
|---|---|---|---|---|
SFINAE / enable_if |
返回类型或模板参数 | 冗长,指向替换失败 | 中 | 语法复杂,约束意图不直观 |
检测惯用法(void_t) |
类型特征 | 中等 | 高 | 需要为每种能力写 trait,样板代码多 |
if constexpr 探测 |
函数体 | 局部较清晰 | 低 | 只能影响当前函数,不能约束重载 |
| requires 表达式 | 概念、if constexpr、static_assert |
较清晰 | 高 | C++20 起可用;只检查有效性,不检查语义 |
跨标准演进:C++20 通过 P0734R0 引入 concepts 与 requires 表达式,取代了大部分 SFINAE 检测惯用法。截至 C++23,这套核心规则没有变化;后续的约束设施仍建立在 requires 表达式之上。