校验逻辑应内聚于接口契约、空对象模式和入口校验三层:c++26 [[expects]] 将前置条件写入函数签名;nulllogger 等空对象消除判空冗余;外部输入必须在首道入口完成完整校验并构造可信结构体。

把校验逻辑收进接口契约里,而不是让每个调用方自己写 if
用 C++26 [[expects]] 把前置条件写进函数签名
校验规则散落,本质是责任错配:本该由被调用方声明“我只接受什么”,却变成调用方被迫记住“我得先检查什么”。C++26 的契约机制直接解决这个根本问题。
-
[[expects]]是编译器可识别的语义断言,不是注释,也不是运行时assert—— 它属于函数接口的一部分 - 例如:
int parse_port(const std::string& s) [[expects: !s.empty() && s.find_first_not_of("0123456789") == std::string::npos]],调用方看到这个声明,就知道传空串或含字母的串是非法使用,不是“运气不好才崩溃” - 编译器在
-fcontract-check模式下会在入口自动插入检查,失败时触发统一处理流程(比如抛std::contract_violation),不用每个调用点重复写if (s.empty()) throw ...
空对象模式替代“判空 + 校验”的双重负担
当校验目标是“某个依赖是否可用”,比如日志器、通知器、配置句柄,传统做法是调用方先 if (logger) 再 logger->log(),既冗余又易漏。空对象模式让校验消失于无形。
- 定义抽象接口
Logger,提供真实实现FileLogger和空实现NullLogger(所有方法空体、不抛异常、不访问任何资源) - 工厂或 DI 容器根据配置返回其中一种,调用方永远拿到合法对象,
logger->log("msg")可直接调,无需判空,也无需再校验logger是否“有效” - 关键点:
NullLogger必须是Logger的完全合规实现,不能偷懒用std::optional<logger></logger>或std::unique_ptr<logger></logger>包裹——那只是把判空从调用方挪到工厂内部,没解决问题
输入校验必须发生在模块边界,且只做一次
校验散落最顽固的场景,是跨模块/跨进程的数据流入点。比如网络包解析后交给业务模块,结果每个业务函数都自己检查字段长度、指针非空、枚举值范围……这纯粹是重复劳动。
- 所有外部输入(网络、文件、命令行、IPC)必须在**第一道入口函数**完成完整校验,校验失败立即拒绝,不向内传递半字节可疑数据
- 入口函数应构造一个**可信的中间结构体**(如
ParsedRequest),其字段已保证非空、长度合法、枚举值在范围内;后续所有内部函数只操作这个结构体,不再重复检查原始 buffer - 避免在内部函数里对
const char*做strlen或memcpy前再检查长度——那是入口函数失职,不是内部函数该补的锅
真正难的不是写校验代码,而是守住边界:契约写在接口上,空对象消解“有无”歧义,入口校验拦住所有脏数据。一旦这三层立住,调用方就真的可以放心调用,而不是边调边猜“这次我是不是又忘了 check”。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











