error_code

1. 为什么需要 error_category

在 C++ 开发中,我们经常遇到从底层(操作系统、网络库、第三方库)返回的 int 类型的错误码。

核心问题:数字是有歧义的。

  • 错误码 1Linux 系统 中可能表示 “Operation not permitted” (EPERM)。

  • 错误码 1HTTP 协议 中可能完全不是这个意思。

  • 错误码 1你的自定义库 中可能表示“初始化失败”。

如果只给你一个数字 1,你无法知道它到底是什么错误。std::error_category 的作用就是给这个数字赋予“上下文(Context)”

2. 它是如何工作的?

C++ 使用 std::error_code 对象来表示一个具体的错误,它内部包含两部分:

  1. Value (int): 错误的数值(例如 1, 404, 500)。

  2. Category (const std::error_category\*): 一个指向分类对象的指针,标明这个数字属于哪个领域。

\text{std::error\_code} = \text{整数值} + \text{错误类别指针}

判断两个错误是否相等时,不仅比较数值,还要比较类别。

3. 标准库自带的类别

C++ 标准库预定义了几个常见的 error_category(它们都是单例):

  1. std::generic_category(): 对应标准的 POSIX 错误码(如 errno)。

  2. std::system_category(): 对应操作系统的底层错误(在 Windows 上可能是 Win32 API 错误,在 Linux 上通常和 generic 一样)。

  3. std::iostream_category(): 用于 IO 流的错误。

4. 代码示例:同样的数字,不同的含义

这个例子展示了即使错误数值都是 1,因为类别不同,它们被视为不同的错误。

#include <iostream>
#include <system_error>
#include <string>

int main() {
    // 创建两个错误码,数值都是 1,但类别不同
    std::error_code ec_system(1, std::system_category());
    std::error_code ec_generic(1, std::generic_category());

    std::cout << "错误 1 (System): " << ec_system.message() << std::endl;
    std::cout << "错误 1 (Generic): " << ec_generic.message() << std::endl;

    // 比较它们
    if (ec_system == ec_generic) {
        std::cout << "它们是同一个错误" << std::endl;
    } else {
        std::cout << "它们是不同的错误 (因为 category 不同)" << std::endl;
    }
    
    // 获取类别名称
    std::cout << "Category Name: " << ec_system.category().name() << std::endl;

    return 0;
}

5. 进阶:如何自定义 error_category

这是 error_category 最强大的地方。如果您在写一个库(比如叫 MyLib),您不应该直接返回 int,也不应该借用系统的错误码。您应该定义自己的类别。

实现步骤如下:

  1. 继承 std::error_category

  2. 实现 name() 方法(返回类别名称)。

  3. 实现 message(int) 方法(根据错误码返回对应的字符串解释)。

  4. 定义一个单例函数来获取这个类别。

// 简化的自定义类别示例
class MyLibCategory : public std::error_category {
public:
    const char* name() const noexcept override {
        return "MyLib";
    }

    std::string message(int ev) const override {
        switch (ev) {
            case 1: return "MyLib: 初始化失败";
            case 2: return "MyLib: 网络连接断开";
            default: return "MyLib: 未知错误";
        }
    }
};

// 获取单例
const std::error_category& my_lib_category() {
    static MyLibCategory instance;
    return instance;
}

int main() {
    // 创建一个属于我们自己库的错误
    std::error_code my_err(2, my_lib_category());
    
    std::cout << "错误信息: " << my_err.message() << std::endl;
    // 输出: 错误信息: MyLib: 网络连接断开
    return 0;
}

简单来说:

  1. error_category 负责定义错误的身份(是系统错误?还是网络库错误?)。

  2. std::system_error 是一个容器,它把 error_category + 错误码 包装成一个异常对象。

  3. exception_ptr 负责把这个异常对象在线程之间搬运


6.联合使用的工作流

这种模式通常用于:底层(基于错误码的)API 在子线程报错,需要将具体的错误码和类别完整地传回主线程处理。

流程步骤:

  1. 产生错误:底层 API 返回一个 int 错误码。

  2. 打包:使用 std::error_code (包含 interror_category) 创建一个 std::system_error 异常并抛出。

  3. 捕获与保存:在子线程捕获该异常,用 std::current_exception 存入 exception_ptr

  4. 传递:将指针传给主线程(通过 std::promise 或共享变量)。

  5. 解包与处理:主线程 rethrow,捕获 std::system_error,然后从中取出原始的 error_category 进行精确判断。


代码示例

这个例子模拟了一个子线程调用底层 OS API(返回错误码),然后主线程精准识别该错误的场景。

#include <iostream>
#include <thread>
#include <future>
#include <system_error>
#include <fstream>

// 模拟一个底层的、基于错误码的函数
// 比如它返回 errno 风格的错误码
int lowLevelOsOperation() {
    // 模拟错误:2 通常代表 ENOENT (No such file or directory)
    return 2; 
}

void workerThread(std::promise<void> prom) {
    try {
        int err = lowLevelOsOperation();
        if (err != 0) {
            // 【关键点 1】将 错误码 + 类别 打包成 std::system_error 抛出
            // 这里我们明确指明这是 generic_category (POSIX 标准错误)
            throw std::system_error(std::error_code(err, std::generic_category()), "读取底层文件失败");
        }
        prom.set_value();
    } catch (...) {
        // 【关键点 2】捕获异常并转为 exception_ptr (存入 promise)
        prom.set_exception(std::current_exception());
    }
}

int main() {
    std::promise<void> prom;
    std::future<void> fut = prom.get_future();

    std::thread t(workerThread, std::move(prom));

    try {
        // 等待结果,如果有异常会在这里重新抛出
        fut.get();
    } 
    catch (const std::system_error& e) { // 【关键点 3】专门捕获 system_error
        // 【关键点 4】拆包:获取原始的错误码和类别
        const std::error_code& ec = e.code();

        std::cout << "--- 主线程捕获系统错误 ---" << std::endl;
        std::cout << "错误描述: " << e.what() << std::endl;
        std::cout << "错误数值: " << ec.value() << std::endl;
        std::cout << "错误类别: " << ec.category().name() << std::endl;

        // 精确判断:不仅判断数值,还判断类别
        if (ec.category() == std::generic_category() && ec.value() == 2) {
            std::cout << ">> 判定原因:找不到指定的文件。" << std::endl;
        }
    } 
    catch (const std::exception& e) {
        std::cout << "捕获到普通异常: " << e.what() << std::endl;
    }

    t.join();
    return 0;
}

为什么要这么麻烦?(优势)

如果只用 std::runtime_error("error 2") 传递字符串,主线程就得解析字符串来猜错误原因,非常脆弱。

联合使用 exception_ptr + system_error (含 error_category) 的好处是:

  1. 类型安全:主线程可以拿到原始的 int 错误码,而不是文本。

  2. 上下文明确:主线程知道这个 2generic_category 里的 2(文件不存在),而不是 iostream_category 里的 2

  3. 无损传递:异常从子线程到主线程,就像在同一个线程里抛出一样,保留了所有的元数据。

这种模式是 C++ 开发高性能服务器(如基于 Asio 网络库)时的标准错误处理范式。

发表评论