C++requires子句定义接口契约
C++20 的 requires 子句:不是语法糖,是接口契约的“白纸黑字”
写模板代码时,你有没有过这种经历?
——函数明明只打算接受能相加、能比较、能拷贝的对象,结果编译器直到实例化那一刻才报错,错误信息像天书一样堆满终端;或者更糟,某些类型“碰巧”通过了编译,却在运行时行为诡异,比如自定义类型重载了 + 但语义是拼接字符串,而你的算法默认它满足交换律……
C++20 的 requires 子句,就是为这类问题按下的“契约确认键”。它不单是让模板约束更“好看”的语法升级,而是把隐含的接口约定,明文写进函数签名里——就像租房合同里白纸黑字写着“不得擅自改造承重墙”,而不是等房东上门才发现你拆了梁。
requires 的本质,是让编译器在模板解析阶段(而非实例化后)就验证调用者是否满足前提条件。这背后不是魔法,是概念(concepts)驱动的静态断言机制。但关键在于:你写的 requires 表达式,就是你对参数行为的最小承诺集。
比如,一个计算容器中最大值的函数,传统写法可能这样:
template<typename T>
auto max_element(const std::vector<T>& v) {
if (v.empty()) return T{};
auto res = v[0];
for (size_t i = 1; i < v.size(); ++i)
if (v[i] > res) res = v[i];
return res;
}
它暗含了三个事实:T 支持 operator>、支持拷贝构造、支持默认构造。但这些全靠“心照不宣”。换成 requires,你可以把它摊开来讲:
template<typename T>
requires std::equality_comparable<T> &&
std::copy_constructible<T> &&
std::default_constructible<T>
auto max_element(const std::vector<T>& v) { /* ... */ }
注意:这里没用 std::totally_ordered<T>,因为 max_element 实际只需要 > 可比较(即 a > b 有定义且返回布尔),并不需要 <, <=, >= 全套——契约越精准,误伤越少。强行套大概念,反而会让本可工作的类型(比如只重载了 > 的轻量结构体)被拒之门外。
更实用的写法,是直接写语义化约束:
template<typename T>
auto max_element(const std::vector<T>& v)
requires requires(const T& a, const T& b) {
{ a > b } -> std::convertible_to<bool>;
{ T{} } -> std::same_as<T>;
}
{ /* ... */ }
这段 requires 不依赖标准库概念,而是直指核心操作:能比较、能默认构造。它像一份微型接口说明书,别人一眼就能看出这个函数真正吃哪几根“骨头”,而不是翻半天文档猜。
有人觉得 requires 是给编译器看的,人读着费劲。其实恰恰相反——当你在头文件里看到:
template<typename Container>
void process(Container&& c)
requires requires(Container&& c) {
{ c.begin() } -> std::input_iterator;
{ c.size() } -> std::integral;
{ c.front() } -> std::same_as<typename Container::value_type&>;
};
你立刻明白:这个函数要迭代、要查长度、要取首元素。它不要求 Container 是 std::vector,也不要求它支持随机访问,但必须提供这三个能力。契约清晰了,实现和调用才能解耦。你改实现时,只要守住这三条线,调用方完全不用动。
实际开发中,requires 最大的价值常出现在“边界模糊”的场景。比如一个日志模块接收任意可序列化类型:
template<typename T>
void log(const T& t)
requires requires(const T& x) {
{ to_string(x) } -> std::convertible_to<std::string>;
}
这里没用 std::to_string(它只支持内置类型),而是调用用户自定义的 to_string 函数——只要存在、可调用、返回 std::string,就过关。requires 能自然承载 ADL(参数依赖查找)语义,这是传统 enable_if 做不到的简洁性。
当然,requires 不是银弹。它不能检查运行时逻辑(比如除零),也不能替代单元测试。但它把“不该发生的错误”,提前到编辑器提示阶段——你在写 log(my_custom_type{}) 时,如果 my_custom_type 没提供 to_string,IDE 就会划红线,而不是等 CI 编译失败。
最后一点实在建议:别一上来就堆砌所有可能约束。先写最简 requires,只覆盖当前函数真正用到的操作;等接口稳定、需求明确后,再逐步收紧。契约太松,失去意义;太紧,扼杀复用。好的接口契约,是呼吸感十足的约束,不是密不透风的牢笼。
下次写模板前,停两秒问自己:这个函数,到底“要求”参数做什么?把答案用 requires 写下来——不是为了炫技,而是为了让代码说人话,让协作少点猜测,多点确定。


还没有评论,来说两句吧...