错误码适用于系统级、性能敏感或无法抛异常的场景(如C接口、中断处理、嵌入式裸机);异常适用于现代C++应用逻辑,更安全、易组合、能准确表达“真正意外”。

错误码适合系统级、性能敏感或无法抛异常的上下文(如 C 接口、中断处理、嵌入式裸机);异常适合绝大多数现代 C++ 应用逻辑,它更安全、更易组合、更能表达“真正意外”的语义——这正是 Herb Sutter 在 GotW #91 和 C++ 委员会讨论中反复强调的立场。
什么时候必须用 error code?
当调用栈中存在无法捕获异常的边界时,error_code 或整型错误码是唯一选择:
- C ABI 兼容接口(如导出给 Python 或 Rust 调用的函数),
extern "C"函数不能抛异常 - 信号处理函数(
signalhandler)、中断服务例程(ISR),异常机制在这些上下文中未定义行为 - 内存极度受限环境(如某些嵌入式 RTOS),禁用异常运行时(
-fno-exceptions)后,std::error_code和std::expected(C++23)成为替代方案 - 需要精确控制错误传播路径(例如:逐层检查并记录每个子操作的
errno),而异常会跳过中间栈帧
为什么异常在应用层更可靠?
Herb Sutter 指出,用错误码“手动传播”极易遗漏检查,而异常强制你面对错误——不是靠约定,而是靠语言机制:
- 忘记检查
if (fd == -1)是静默 bug;未捕获异常则程序终止(可配std::set_terminate),问题立刻暴露 - 异常支持资源自动释放(RAII),即使多层嵌套调用也能保证
std::unique_ptr、锁、文件句柄被析构;错误码需手动写goto cleanup或重复if,易出错 -
std::exception_ptr和std::nested_exception可跨线程传递错误上下文;错误码只能传数字,丢失类型、消息、堆栈线索
std::error_code 不是异常的“轻量替代”,而是协作桥梁
Herb 明确反对把 std::error_code& 当作“不抛异常的异常”来滥用。它的设计目标是与系统 API 互操作,不是替代 throw:
立即学习“C++免费学习笔记(深入)”;
-
std::error_code本身不带堆栈、不支持动态多态;std::system_error才是它的异常封装,该抛时就该抛 -
标准库 I/O(如
std::ifstream::open())默认抛异常;但提供std::ios_base::failbit+clear()模式供 error-code 用户切换——这是分层设计,不是二选一 - 混合使用时,推荐:底层系统调用返回
error_code,上层业务逻辑将其转为具名异常(如throw std::filesystem::filesystem_error(...))
// 正确协作:将 error_code 转为语义化异常
void open_config_file(const std::string& path) {
std::error_code ec;
std::ifstream f{path, std::ios::in | std::ios::binary};
if (!f.good()) {
std::filesystem::status(path, ec); // 系统调用返回 ec
if (ec) {
throw std::filesystem::filesystem_error(
"cannot open config", path, ec
);
}
throw std::runtime_error("config file empty or invalid");
}
}
真正难的是界定“意外”——网络超时是意外还是预期分支?Herb 的建议很实在:如果这个错误在绝大多数调用中都不该发生,且调用方无法合理恢复,就该用异常;如果它是常规控制流的一部分(比如“键不存在”之于哈希查找),std::optional 或 std::expected 更贴切。别让错误处理逻辑模糊了业务意图。








