因为std::exception无法区分错误语义层级,需用枚举定义warn/error/fatal级别,通过派生类实现类型化捕获,并确保what()缓存字符串避免悬垂指针。

为什么不用 std::exception 而要自己封装错误码级别?
因为 std::exception 只提供统一的 what() 字符串,无法直接区分「警告」「可恢复错误」「致命错误」等语义层级;日志系统、监控告警或上层策略(比如重试逻辑)需要明确知道错误严重性,而不是靠解析字符串匹配关键词。
实操建议:
- 错误码用枚举定义,每个值对应一个明确级别(如
WARN、ERROR、FATAL),避免 magic number - 不要把错误码塞进
std::runtime_error的 message 里——那样会丢失类型信息,且无法在catch中按级别做分支处理 - 继承
std::exception是为了兼容现有 catch 块,但必须重写what()返回含级别前缀的字符串,方便调试时一眼识别
如何设计带级别的异常类结构?
核心是让异常对象自带级别字段,并支持按级别捕获。不推荐用虚函数做“级别 dispatch”,而应靠多态 + 类型区分更清晰、更高效。
实操建议:
- 定义基类
ErrorCodeException,含enum class Level成员和level()访问器 - 为每个常用级别(如
WARN、ERROR)派生具体类:WarnException、ErrorException,构造时固定级别,避免运行时误设 - 所有子类都显式继承
std::exception,并实现what(),返回类似"[ERROR] connect timeout"的格式 - 不提供 public 的 level 设置接口,级别应在构造时确定,防止异常抛出后被篡改
示例关键片段:
enum class ErrorLevel { WARN, ERROR, FATAL };
<p>class ErrorCodeException : public std::exception {
protected:
ErrorLevel level<em>;
std::string msg</em>;
public:
ErrorCodeException(ErrorLevel l, const std::string& m) : level<em>(l), msg</em>(m) {}
ErrorLevel level() const { return level<em>; }
const char* what() const noexcept override {
return (std::string{ "[" } + levelStr(level</em>) + "] " + msg_).c_str();
}
private:
std::string levelStr(ErrorLevel l) const {
switch(l) {
case ErrorLevel::WARN: return "WARN";
case ErrorLevel::ERROR: return "ERROR";
case ErrorLevel::FATAL: return "FATAL";
default: return "UNKNOWN";
}
}
};</p><p>class WarnException : public ErrorCodeException {
public:
WarnException(const std::string& m) : ErrorCodeException(ErrorLevel::WARN, m) {}
};</p><p>class ErrorException : public ErrorCodeException {
public:
ErrorException(const std::string& m) : ErrorCodeException(ErrorLevel::ERROR, m) {}
};</p>
怎么按级别捕获而不漏掉其他异常?
不能只写 catch(const ErrorException&),否则 WarnException 和原始 std::exception 都会被跳过。真实场景中,你既想单独处理错误级异常,又得兜底处理其他所有异常。
实操建议:
- 按从具体到宽泛的顺序排列
catch块:先WarnException/ErrorException,再ErrorCodeException(捕获所有自定义级别异常),最后std::exception或... - 在
ErrorCodeException的catch块里用e.level()做分支,比写一堆同级catch更易维护 - 注意:
what()返回的是临时std::string的c_str(),上面示例中直接拼接并返回是危险的——实际使用需缓存完整字符串,否则返回悬垂指针
哪些地方最容易踩坑?
最隐蔽的问题不在设计,而在生命周期和日志输出一致性。
-
what()中拼接字符串必须持有完整副本,不能返回局部std::string的c_str()—— 上面示例只是示意,真实代码要用mutable std::string what_cache_;缓存结果 - 如果异常要跨线程传递(比如异步任务中 throw),确保所有成员可拷贝,且无裸指针或文件描述符等非平凡资源
- 不要在异常类中做日志打印——异常对象只负责携带信息,日志行为应由
catch处理者决定,否则会导致重复打日志或干扰错误传播路径 - 若集成到已有框架(如 gRPC、Boost.Asio),注意它们可能吞掉异常或强制转换为特定状态码,此时需在顶层
catch中显式映射级别到对应 status code
级别不是越多越好,三个足够覆盖绝大多数服务场景;加第四个(比如 DEBUG)容易导致生产环境误抛,反而掩盖真正问题。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











