95 / 103 · C++17 · 约 12 分钟
错误边界:报告失败不等于处理失败
解析层报告它能证明的错误,业务层才决定如何响应。用一个金额解析接口区分发现、传播和翻译,保留异常的动态类型,并让失败结果不携带伪造金额。
C++ Coding Standards: 101 Rules, Guidelines, and Best Practices
已取得第一版完整 PDF,顺序读完 0–100 全部 101 条的正文(Summary、Discussion、Examples、Exceptions 与 References),并逐页直接查看嵌入代码图像所在的全部 80 个实质页,补齐文本提取留下的代码空白。另交叉阅读官方 Items 1、25、73、74、83。汇总与索引不计正文。auto_ptr、动态异常规范、旧式适配器等仅作历史语境;原创示例采用 C++20,不照搬旧代码。
查看版本、实际阅读范围与原文入口 →先决定谁有足够信息
读到 Item 74 时,值得追问的不是“哪里可以写 catch”,而是“这一层能做什么决定”。底层解析器只能判断文本是不是非负整数,不能替调用者决定坏输入是否等价于零。把失败当成零会把损坏数据变成一笔真实业务记录,程序表面上继续运行,契约却已失效。
例子的 parse_cents 发现格式或范围错误便抛出 ParseError。它不输出日志、不重试,也不读取外部状态;调用者可以独立决定如何展示失败。返回 int 表示成功,异常表示没有金额产生,两条路径不能混为一谈。
传播时保留事实,翻译时添加语义
中间层如果没有恢复动作,通常不需要 catch。这里刻意保留一次按 const 引用捕获再裸 throw 的转发,以便断言演示动态类型仍是 ParseError;实际项目应删除没有作用的转发层。写成 throw e 会创建一个由表达式静态类型决定的新异常,可能丢掉派生类信息。
业务边界 submit 把已知解析失败翻译成 Status::bad_amount,并让 optional 金额为空。它只捕获自己能够解释的 ParseError,而不是用 catch(...) 吞掉内存不足等无关故障。错误翻译不是抹平差异,而是把调用者真正需要的语义放进接口。
现代接口仍然需要边界证明
Item 73 的按值抛出、按引用捕获原则在现代 C++ 仍然适用;C++17 的 from_chars 和 optional 则是本课新增工具,不是 2005 年书中已有设施。from_chars 不保证消费整个输入,所以必须同时检查错误码和结束指针;否则 12x 可能被错当成 12。
示例同时检查合法输入、尾随垃圾和整数溢出。assert 用于证明本例约定,不是线上输入校验的替代物:即使禁用断言,parse_cents 内的分支仍会拒绝错误。若改用 C++23 expected,仍应保持同样的成功与失败边界,而不是因为换了容器就认为错误策略已经完成。
容易答错的地方
- 只检查 from_chars 的 ec 而不检查 ptr,会接受带非法后缀的部分输入。
- catch(const std::exception& e) 后使用 throw e; 可能切掉派生异常;重新抛出当前异常使用 throw;。
运行一个例子
最低标准 C++17 · 完整程序 · 下载 .cpp
#include <cassert>
#include <charconv>
#include <optional>
#include <stdexcept>
#include <string_view>
#include <system_error>
struct ParseError : std::runtime_error {
using std::runtime_error::runtime_error;
};
int parse_cents(std::string_view text) {
if (text.empty()) throw ParseError("empty amount");
int cents = 0;
const char* end = text.data() + text.size();
const auto result = std::from_chars(text.data(), end, cents);
if (result.ec != std::errc{} || result.ptr != end || cents < 0)
throw ParseError("invalid amount");
return cents;
}
int preserve_error(std::string_view text) {
try { return parse_cents(text); }
catch (const std::exception&) { throw; }
}
enum class Status { accepted, bad_amount };
struct Receipt { Status status; std::optional<int> cents; };
Receipt submit(std::string_view text) {
try { return {Status::accepted, parse_cents(text)}; }
catch (const ParseError&) { return {Status::bad_amount, std::nullopt}; }
}
int main() {
const auto good = submit("1250");
assert(good.status == Status::accepted && good.cents == 1250);
const auto bad = submit("12x");
assert(bad.status == Status::bad_amount && !bad.cents);
assert(!submit("9999999999999999999999999999999999999999").cents);
bool preserved = false;
try { (void)preserve_error(""); }
catch (const ParseError&) { preserved = true; }
assert(preserved);
}
在本地编译
g++ -std=c++17 -Wall -Wextra -Wpedantic -pthread books-cpp-coding-standards.cpp -o example && ./example预期结果
预期:正常退出、无输出;所有 assert 通过。
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
让金额 0 也成为业务错误,但不改变底层解析器对合法整数的定义。修改哪里?
查看参考答案
在 submit 调用 parse_cents 后保存结果;若 cents == 0,返回 {Status::bad_amount, std::nullopt},否则返回成功。parse_cents("0") 仍得到 0,因为零是合法整数;业务是否接受零由边界决定。增加 submit("0") 失败且 parse_cents("0") 成功的断言。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。