4.3 Concepts:给账户设准入门槛


4.3 Concepts:给账户设准入门槛

本节摘要:Concept(概念)是类型的可复用资质声明:用 requires 子句列出"能做什么"的编译期检查清单,写进模板签名后,不合规的调用在重载决议阶段被拒绝,错误信息从百行模板噪音变成一句"约束不满足"。它是 SFINAE 查账的制度化替代,也是泛型接口的自文档化条款。

上一节的 traits 与 SFINAE 能查账但难读——条款藏在密文里,报错绕来绕去。本节的 Concepts 把同一件事搬到台面上:资质要求是一等公民,写在签名里、命名成概念、复用于全套接口。本章主线里的路由模板,正好用它把"Key 要支持什么"明明白白立成门禁。

先看一笔账

给路由模板立两道门槛(可哈希、可判等),先看合格调用与不合格调用各自的结局:

#include <cstdio> #include <string> // 定义概念:Key 必须可比较相等,且可转成 size_t(模拟哈希资质) template <typename K> concept RoutableKey = requires(const K& k, std::size_t seed) { { k == k } -> std::convertible_to<bool>; { k.hash_code() } -> std::convertible_to<std::size_t>; }; template <RoutableKey K> // 概念直接当模板参数的"门票" void route(const K& key) { std::printf("路由键通过审计,哈希 %zu\n", static_cast<std::size_t>(key.hash_code())); } struct GoodKey { int id; bool operator==(const GoodKey& o) const { return id == o.id; } std::size_t hash_code() const { return static_cast<std::size_t>(id); } }; struct BadKey { int id; bool operator==(const BadKey& o) const = delete; // 缺判等 }; int main() { route(GoodKey{7}); // 资质齐:放行 // route(BadKey{1}); // 资质缺:重载决议直接拒绝 return 0; }

输出:

路由键通过审计,哈希 7

放开 BadKey 那行,编译器给的报错是这种画风(节选大意):"约束不满足——BadKey 不满足 RoutableKey,因为 operator== 被删除"——一行点名缺什么。对照上一节 SFINAE 的报错(几十行模板展开里埋着一句核心),可读性差距一目了然。

requires 表达式逐项拆解:requires(形参列表) { 要求序列 } 只在编译期做检查、不生成代码。要求序列支持三种写法——简单要求(表达式合法即可,如 k + 1;)、类型要求(有某嵌套类型或成员,如 typename K::iterator;)、复合要求(带返回类型约束的表达式,如上面 { k == k } -> ...)。概念就是给 requires 表达式起的名字,命名后可复用于模板参数、函数签名、变量约束任何位置。

一、门槛的四种用法

概念写进代码的位置不止一处,四种用法各有审计价值:

#include <concepts> #include <string> // 用法一:约束模板参数(前面 route 的写法) template <std::integral T> T clip(T v, T lo, T hi) { return v < lo ? lo : (v > hi ? hi : v); } // 用法二:约束函数参数——requires 子句后置 template <typename T> void log_size(const T& c) requires requires { c.size(); } { std::printf("容器大小 %zu\n", c.size()); } // 用法三:约束返回类型(尾置) template <typename A, typename B> requires std::same_as<A, B> auto pick(bool use_first, const A& a, const B& b) -> const A& { return use_first ? a : b; } // 用法四:非模板变量与成员的常规约束场景里,配合 static_assert 做前置自检 template <typename T> struct Registry { static_assert(std::movable<T>, "注册表元素必须可移动:扩容要搬元素"); }; int main() { std::printf("裁剪结果 %d\n", clip(5, 1, 3)); log_size(std::string("audit")); std::printf("挑中 %d\n", pick(false, 1, 1)); Registry<std::string> ok; // std::string 可移动:通过 // Registry<std::unique_ptr<int>> bad; // unique_ptr 可移动,其实也过; // 换成一个拷贝可以移动不行的类型才会被拦——约束按字面资质判定 return 0; }

输出:

裁剪结果 3 容器大小 5 挑中 1

标准库在头文件 concepts 里预置了整套通用概念:std::integral、std::movable、std::copyable、std::same_as、std::convertible_to 等,与 4.2 的 traits 一一对应——concept 是 traits 的"命名词"。审计建议:优先用标准概念表达通用资质,自定义概念只写业务特有条款(如上面 RoutableKey 的 hash_code 要求),别重复造 std::movable。

二、案例:路由模板的门禁改造

背景:4.2 末尾的资质核查靠人工打印 traits 清单,新类型接入时总有人忘跑核查,错票到运行期测试才暴露。

操作:把核查清单固化成概念:template <typename K> concept RouteKey = std::equality_comparable<K> && std::movable<K> && requires(const K& k) { { k.hash_code() } -> std::convertible_to<std::size_t>; };,路由表与路由函数的模板参数统一写 RouteKey。接入文档从此只有一句:"让你的 Key 满足 RouteKey。"

结果:新同事接入自定义 Key 时漏写了 noexcept 移动,编译错误当场点名"不满足 std::movable"——原来要靠评审与人肉核查才能发现的事,现在类型系统在第一行就拦住。接入平均耗时从半天降到一小时内。

解读:对比 4.2 与 4.3 的两种写法,差别不是"能不能",而是条款存在于谁的记忆里。SFINAE 的条款藏在实现者脑子里(或注释里),Concepts 的条款活在签名里,编译器、IDE、调用方三方共享同一份。这就是"门槛"的价值:把审计从"事后查"变成"进门验"。

变式:概念支持组合与收窄——用 && 拼接多个概念形成更严的门槛,用 ! 排除某资质做分派(如"可哈希走哈希表、否则走线性查找"的重载对)。分派场景里,两个互相排斥的 concept 约束的重载就是 4.2 中 enable_if 对偶的文明写法。

⚠️ 常见坑:把概念写成"永远为真"的摆设。template <typename T> concept Anything = true; 挂在签名上没有提供任何审计价值,反而让读者以为有约束。概念要么表达真实资质,要么不写——空门槛比没有门槛更具误导性。

本节要点回顾

  • concept 是命名的资质清单:requires 表达式逐项检查,命名后全场景复用。
  • 拒绝发生在重载决议:不合格类型进不了函数体,报错点名缺哪项资质。
  • 标准概念优先:std::movable、std::equality_comparable 等与 traits 对应,别重复发明。
  • 四种用法:约束模板参数、后置 requires、尾置返回、static_assert 前置自检。
  • 条款进签名:编译器、IDE、调用方共享同一份资质要求,审计从人肉变机器。
  • 空门槛是误导:概念要么表达真实约束,要么不写。

本章的编译期账房收账:票怎么开、资质怎么查、门槛怎么立。下一章回到运行期,账本第一次交到多个执行流手里——并发账房里,同一笔账被两个柜员同时改写,秩序问题接踵而至。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U