error_category?
在 C++ 开发中,我们经常遇到从底层(操作系统、网络库、第三方库)返回的 int 类型的错误码。
核心问题:数字是有歧义的。
-
错误码
1在 Linux 系统 中可能表示 “Operation not permitted” (EPERM)。 -
错误码
1在 HTTP 协议 中可能完全不是这个意思。 -
错误码
1在 你的自定义库 中可能表示“初始化失败”。
如果只给你一个数字 1,你无法知道它到底是什么错误。std::error_category 的作用就是给这个数字赋予“上下文(Context)”。
2. 它是如何工作的?
C++ 使用 std::error_code 对象来表示一个具体的错误,它内部包含两部分:
-
Value (int): 错误的数值(例如 1, 404, 500)。
-
Category (
const std::error_category\*): 一个指向分类对象的指针,标明这个数字属于哪个领域。
\text{std::error\_code} = \text{整数值} + \text{错误类别指针}
判断两个错误是否相等时,不仅比较数值,还要比较类别。
3. 标准库自带的类别
C++ 标准库预定义了几个常见的 error_category(它们都是单例):
-
std::generic_category(): 对应标准的 POSIX 错误码(如errno)。 -
std::system_category(): 对应操作系统的底层错误(在 Windows 上可能是 Win32 API 错误,在 Linux 上通常和 generic 一样)。 -
std::iostream_category(): 用于 IO 流的错误。
4. 代码示例:同样的数字,不同的含义
这个例子展示了即使错误数值都是 1,因为类别不同,它们被视为不同的错误。
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,也不应该借用系统的错误码。您应该定义自己的类别。
实现步骤如下:
-
继承
std::error_category。 -
实现
name()方法(返回类别名称)。 -
实现
message(int)方法(根据错误码返回对应的字符串解释)。 -
定义一个单例函数来获取这个类别。
// 简化的自定义类别示例
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;
}
简单来说:
-
error_category负责定义错误的身份(是系统错误?还是网络库错误?)。 -
std::system_error是一个容器,它把error_category+错误码包装成一个异常对象。 -
exception_ptr负责把这个异常对象在线程之间搬运。
6.联合使用的工作流
这种模式通常用于:底层(基于错误码的)API 在子线程报错,需要将具体的错误码和类别完整地传回主线程处理。
流程步骤:
-
产生错误:底层 API 返回一个
int错误码。 -
打包:使用
std::error_code(包含int和error_category) 创建一个std::system_error异常并抛出。 -
捕获与保存:在子线程捕获该异常,用
std::current_exception存入exception_ptr。 -
传递:将指针传给主线程(通过
std::promise或共享变量)。 -
解包与处理:主线程
rethrow,捕获std::system_error,然后从中取出原始的error_category进行精确判断。
代码示例
这个例子模拟了一个子线程调用底层 OS API(返回错误码),然后主线程精准识别该错误的场景。
// 模拟一个底层的、基于错误码的函数
// 比如它返回 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) 的好处是:
-
类型安全:主线程可以拿到原始的
int错误码,而不是文本。 -
上下文明确:主线程知道这个
2是generic_category里的2(文件不存在),而不是iostream_category里的2。 -
无损传递:异常从子线程到主线程,就像在同一个线程里抛出一样,保留了所有的元数据。