把异常关进抽屉:一次用 std::error_code 重构日志库的笔记

背景

公司代码规范新加一条:“核心库禁用异常。”
于是我把原本四处 throw 的日志库翻出来,决定用 std::error_code 做一次“无异常化”翻新。过程顺手记下,权当备忘。

选型思路

异常一旦禁用,能选的只剩三样:

  1. 返回 bool——信息太少;
  2. 输出 int errno——命名空间污染;
  3. std::error_code——值语义、可扩展、与标准库同频。

结论:直接上积木。

旧接口

// 旧日子:抛就完事
void rotate_log(const std::string& path);
// 调用方
try {
    rotate_log("app.log");
} catch (const LogException& e) {
    std::cerr << e.what() << '\n';
}

新接口

// 新日子:把错误码塞回来
std::error_code rotate_log(const std::string& path) noexcept;
// 调用方
if (auto ec = rotate_log("app.log"); ec) {
    std::cerr << ec.message() << '\n';
}

错误值设计

先列可能出错的情节:

  • 文件根本不存在 → not_found
  • 权限不足 → permission_denied
  • 磁盘写满 → no_space
  • 成功 → ok(必须是 0)

enum class 一次性写死:

enum class LogErr {
    ok = 0,
    not_found,
    permission_denied,
    no_space
};

错误类别

class LogCategory : public std::error_category {
public:
    const char* name() const noexcept override { return "log"; }
    std::string message(int ev) const override {
        switch (static_cast<LogErr>(ev)) {
        case LogErr::ok:               return "success";
        case LogErr::not_found:        return "log file not found";
        case LogErr::permission_denied:return "permission denied";
        case LogErr::no_space:         return "disk full";
        default:                       return "unknown log error";
        }
    }
};

inline const std::error_category& log_category() {
    static LogCategory c;
    return c;
}

让枚举自动变身

namespace std {
template<> struct is_error_code_enum<LogErr> : true_type {};
}

inline std::error_code make_error_code(LogErr e) {
    return {static_cast<int>(e), log_category()};
}

实现函数

std::error_code rotate_log(const std::string& path) noexcept {
    if (!fs::exists(path)) return LogErr::not_found;
    fs::space_info si = fs::space(path);
    if (si.available < 1_MiB) return LogErr::no_space;
    std::error_code ec;
    fs::rename(path, path + "." + timestamp(), ec);
    return ec ? LogErr::permission_denied : LogErr::ok;
}

注意:内部依旧用 fs:: 的无异常重载,把系统级错误转成自家枚举,保持对外统一语言。

调用侧

for (auto& path : log_files) {
    if (auto ec = rotate_log(path); ec) {
        report_to_monitoring("rotate failed", ec);
        continue;  // 单文件失败不阻断批次
    }
}

没有 try/catch,代码路径平坦;监控端收到的 ec.value() 可直接映射到告警级别。

单元测试

TEST(rotate_log, not_found) {
    auto ec = rotate_log("missing.log");
    EXPECT_EQ(ec, LogErr::not_found);
    EXPECT_TRUE(ec);
    EXPECT_EQ(ec.message(), "log file not found");
}

用枚举值直接比较,测试用例一眼能读。

经验小结

  1. 先列“失败场景”再列“枚举值”,保证不遗漏、不重复。
  2. 一定把 ok 钉死在 0,否则 if (ec) 就全乱。
  3. 内部若调用其他 error_code 接口,就地转换,别让系统错误泄漏到上层。
  4. 消息串只给人看,逻辑判断永远用枚举或布尔。

结语

把异常关进抽屉后,std::error_code 成了最顺手的那块积木:轻量、可拷贝、能跨线程、还能与标准库拼成同一套“语言”。一次重构,日志库体积没涨,性能没跌,代码审查却少了一堆 “catch (...) 兜底” 的争吵——也算意外收获。

posted @ 2025-12-30 10:25  VirboxProtector  阅读(3)  评论(0)    收藏  举报