继承std::exception并添加错误码成员是最直接的方式:定义含m_error_code成员和code()访问函数的类,避免字符串解析;用int或std::error_code(配全局唯一category);抛出用throw XxxException(...),捕获用const XxxException&引用。

继承 std::exception 并添加错误码成员是最直接的方式
标准库不提供带错误码的异常基类,所以得自己造一个。关键不是“能不能抛”,而是“抛出去后调用方能不能安全、一致地拿到错误码”。直接在自定义类里加一个 m_error_code 成员(比如 int 或 std::error_code),并提供公开的 code() 访问函数,比靠字符串解析或动态类型判断靠谱得多。
常见错误是只重写 what() 并把错误码塞进字符串里——这样调用方必须做字符串匹配或正则提取,既脆弱又低效。
- 错误码类型优先选
int(简单场景)或std::error_code(需对接系统错误或分类时) - 构造函数必须能接收错误码,并确保它被存储;别忘了默认构造或拷贝构造时保持成员一致性
-
what()可以返回格式化后的消息,但不要让它成为唯一信息出口
用 std::error_code 时注意类别(category)必须全局唯一
如果选 std::error_code,光有值不够,还得配一个 std::error_category 子类。否则 error_code.value() 虽然能取到数,但 error_code.category() 会出错,== 比较也会失效——因为两个不同实例的 category 对象地址不同,std::error_code 默认按 category 地址判等。
典型翻车现场:在头文件里定义匿名 std::error_category 子类,然后多个 .cpp 包含它,结果链接时 category 对象重复,比较永远为 false。
立即学习“C++免费学习笔记(深入)”;
- category 对象必须声明为
static全局变量,或用函数局部静态对象保证单例 - 重写
name()返回稳定字符串(如"myapp"),message()按值返回描述 - 别在异常类里 new 一个 category 实例——生命周期和线程安全都成问题
抛出和捕获时避免切片(slicing)和裸指针管理
写 throw MyException(404); 没问题,但若写 throw new MyException(404); 就危险了:异常对象必须由栈或异常机制自动管理,手动 new 出来会导致内存泄漏(catch 块里没 delete 的机会)。
另一个坑是捕获时用基类值类型:catch (std::exception e) ——这会触发对象切片,自定义的 code() 成员直接消失。必须用引用:catch (const MyException& e) 或至少 catch (const std::exception& e)(但后者拿不到你的错误码,除非你把它塞进 what() 字符串里,又绕回第一坑)。
- 抛出:永远用
throw XxxException(...);,不用指针、不用std::move - 捕获:优先捕获具体异常类型引用,如
catch (const MyException& e) { e.code(); } - 如果必须统一捕获,考虑让所有异常都继承一个带
virtual int code() const = 0;的基类
错误码语义要明确,别和系统 errno 混用
自定义错误码最好自成体系,比如用负数表示业务错误(-1001 用户不存在,-1002 权限不足),正数留给系统调用返回值。混用 errno 值容易误导:open() 失败抛出 MyException(EACCES),但调用方看到 13 并不能立刻判断这是系统权限错误还是你自定义的“配置文件不可读”。
更隐蔽的问题是跨平台:Windows 的 ERROR_ACCESS_DENIED 是 5,Linux 的 EACCES 是 13,直接透传会让错误码失去可读性和可维护性。
- 定义枚举或常量集,如
enum class MyAppErrc { kUserNotFound = -1001, kPermissionDenied = -1002 }; - 在异常构造时显式转换,而不是依赖隐式
int构造 - 文档里写清每个码的含义、触发条件、是否可重试——错误码不是数字游戏


















