1 自定义 error_condition
上一节我们实现了自定义的 error_code,通过定制 std::error_code 的内部机制,支持与系统相关错误码的隐式转换和直接比较,这一节我们再考虑一种情况。假设我们的支持库要同时支持 Windows 和 Linux 系统,设备厂家分别为两种操作系统提供了设备驱动程序,但是它们底层的错误码却并不一致。以访问被拒绝错误为例,Windows 系统上的设备驱动会返回 0xC0000005,而 Linux 系统上的设备驱动会返回 13。支持库要求同时支持两种操作系统,希望提供一种与设备无关的错误码定义,让支持库不需要根据操作系统的差异用两套代码判断错误码,这种情况就需要使用自定义的 error_condition。
与 error_code 的设计一样,error_condition 也用了 pimpl 大法,表面就是一张皮,内部全看 error_category。
1.1 定义错误码
根据需求,我们要定义一组与系统无关的错误码,着需要有一定的抽象思考。作为一个简单的例子,我们将两种系统底层错误码抽象为以下两个:
enum class devsys_error {device_busy = 1,no_permission = 2
};
因为是定义系统无关的错误码,所以不用考虑跟底层错误码冲突的问题,我们简单用非 0 的值为它们编码。接着要提供 is_error_condition_enum<> 的特化版本,让系统知道我们的 devsys_error 是与系统无关的 error_condition 类型错误码:
template <>
struct std::is_error_condition_enum<devsys_error> : std::true_type {};
1.2 实现 error_category
要支持 error_condition 的等价性判断,需要为自定义的 error_condition 提供与之相适应的 error_category。上一节已经介绍过 name() 接口和 message() 接口的实现方法,这里照样实现就行了:
struct devsys_errtab_t
{devsys_error code;const char* message;
};constexpr devsys_errtab_t devsys_errtab[] = {{devsys_error::device_busy, "Internal device is busy"},{devsys_error::no_permission, "Permission is deny"}
};class devsys_error_category : public std::error_category {
public:const char* name() const noexcept override {return "devsys_error";}std::string message(int condition) const override {for (const auto& entry : devsys_errtab) {if (static_cast<int>(entry.code) == condition) {return entry.message;}}return "unknown error";}
};
本篇第二部分为实现自定义 error_code 而做的 dev_error_category 不需要考虑两个 equivalent() 接口的实现,因为我们自定义的 error_code 不需要考虑跟其他 error_condition 错误码进行等价性判断,使用 error_category 默认的行为就行了。但是要实现自定义的 error_condition,就需要考虑自身与其他 error_code 之间进行等价性判断,并且这个等价的定义要自己定义。定义等价性最简单的方法就是做个映射表,将不同平台的错误码值映射成我们定义的 devsys_error。根据之前的讨论,一个简单的映射表出现了:
struct devsys_map_t {int dcode;devsys_error icode;
};constexpr devsys_map_t devsys_map[] = {{ 0xC000002A, devsys_error::device_busy},{ 9, devsys_error::device_busy },{ 0xC0000005, devsys_error::no_permission},{ 13, devsys_error::no_permission }
};
错误码的值都是杜撰的,如有雷同,实属巧合。当然,也可以根据平台的差异分别定义 devsys_map,比如:
#ifdef _WIN32
constexpr devsys_map_t devsys_map[] = {{ 0xC000002A, devsys_error::device_busy},{ 0xC0000005, devsys_error::no_permission}
};
#else
constexpr devsys_map_t devsys_map[] = {{ 9, devsys_error::device_busy },{ 13, devsys_error::no_permission }
};
#endif
好处就是减少了映射表的大小,查找的时候效率更高一点。
equivalent() 接口有两个重载形式,第一个用于判断一个系统相关的错误码值与一个 error_condition 对象实例是否等价。实现的方法就是遍历 devsys_map,找到与 code 等值的映射关系,看看映射关系中的 devsys_error 是否与给定的 error_condition 相等:
bool equivalent(int code, const std::error_condition& condition) const noexcept override {for (const auto& entry : devsys_map) {if (entry.dcode == code) {return (condition == std::error_condition(entry.icode));}}return false;
}
error_condition() 的构造函数根据查找的映射关系中的 devsys_error 构造一个 error_condition() 对象实例,然后与第二个参数给出的 condition 对象比较,两个 error_condition 之间的比较是严格等值比较,这是 error_condition 自身的行为,没有用到 error_category。第二个 equivalent() 接口用于判断一个 error_code 对象实例与一个系统无关的错误码值是否等价。因为要与 error_code 对象进行比较,我们需要将映射表中的错误码(dcode)转换成 error_code 对象,然后与 code 参数做严格等值比较查找映射条目。如果找到映射条目,就比较相对应的系统无关错误码(icode)是否与第二个参数 condition 一致:
bool equivalent(const std::error_code& code, int condition) const noexcept override {for (const auto& entry : devsys_map) {if (std::error_code(entry.dcode, std::system_category()) == code) {return (static_cast<int>(entry.icode) == condition);}}return false;
}
这个例子中,我们没有自定义 error_code,所以构造 error_code 的时候使用了 C++ 的 system_category 分类,其实这里 system_category 只是相当于一个占位符,因为 error_code 之间比较用不到 error_category(既然是占位符,使用 generic_category 或 iostream_category 都可以)。当需要与 error_condition 比较的时候,error_condition 的 devsys_error_category 分类就参与进来了。其实如果严谨一点,或者正式一点的设计,比如考虑跨平台发开,一般都要同时自定义 error_code 和 error_condition。如果同时自定义了 error_code,这里的判断就不需要这么复杂,可以直接将 entry.dcode 与 code 参数做 == 判断( if(entry.dcode == code) ),因为自定义的 error_code 构造函数会将 dcode 隐式转换成 error_code(当然,dcode 就不能用整数类型了)。
最后,我们也要提供一个全局函数,返回一个 devsys_error_category 引用,下一节实现 make_error_condition() 函数的时候会用到它:
const std::error_category& devsys_category() {static devsys_error_category instance;return instance;
}
1.3 满足 error_condition 构造
根据 error_condition 的构造函数,将一个 devsys_error 类型错误码枚举值隐式转型成 error_condition 对象需要满足两个条件,第一个是提供 is_error_condition_enum<devsys_error> 特化版本定义,这个我们在 3.1 节已经给出。第二个条件就是提供 make_error_condition() 函数针对 devsys_error 类型的重载实现。当错误码类型是 devsys_error 时,用 devsys_category 构造一个 error_condition 对象:
std::error_condition make_error_condition(devsys_error e) noexcept {return std::error_condition( static_cast<int>(e),devsys_category() );
}
1.4 验证一下
现在假设我们的支持库中有一个功能函数,可能返回与系统相关的错误码,为了测试方便,我们通过 code 参数控制它返回的错误码的值:
bool MyOperator2(int code, std::error_code& ec) {ec = std::error_code(code, std::system_category());return false;
}
注意这里构造与系统相关的错误码时也用了 system_category,原因 1.2 小节已经介绍过了,因为我们这个例子没有使用自定义 error_code,所以借用 system_category 类型做占位符构建与系统相关的错误码。其实用任何 error_category 类型都可以,只要与 equivalent() 函数一致就行了。
测试代码如下:
std::error_code ec1, ec2;
MyOperator2(0xC0000005, ec1);
MyOperator2(13, ec2);assert(ec1 != ec2);
assert(ec1 == devsys_error::no_permission);
assert(ec2 == devsys_error::no_permission);
正如我们预期的那样,ec1 和 ec2 虽然都是底层可能返回的权限拒绝错误码,但是它们是与系统相关的,所以 ec1 不等于 ec2。但是由于我们自定义 error_condition,以及相应的 devsys_category 提供了等价性判断,使得 ec1 和 ec2 都被认为是没有操作权限的错误。
2 总结
通过自定义 error_code 和 error_condition 这两个例子,我们将 C++ 的 error_code 的设计灵活性具体展现给大家。error_code 的两个原则就是既支持“用户可扩展”,又能够“保留原始错误代码”的详细信息。标准库的用户能够添加自己的错误源,可能用于集成第三方库,也可用于创建更高级别的错误抽象定义。与此同时,程序员将使用系统底层的错误代码进行日志记录和跟踪,在诊断问题时可能起到至关重要的作用。比如上一节的例子,向上可以统一报告用户没有操作权限的错误,向下可以将 ec1 或 ec2 中具体的错误码写入 log,以方便诊断错误的具体原因。
参考资料
[1] Christopher Kohlhoff. system-error-support-in-c0x.
[2] N2241: Diagnostics Enhancements for C++0x (Rev. 1)
[3] https://en.cppreference.com/w/cpp/error/error_code