std::error_code 和它的朋友们
分类:技术
前几天看 API 文档时候遇到了 std::error_code
这个东西,当时以为是 errno
的 Alias,后续查阅文档才发现并没有那么简单。
system_error
std::error_code
包含在 <system_error>
头文件中,相关的共有四个组件:
std::system_error
std::error_code
std::error_condition
std::error_category
std::system_error
是一个继承自 std::runtime_error
的异常类,用以表示与 OS 交互时得到的以错误码形式返回的错误。因此除了提供标准的 what()
函数外,它还额外暴露了一个返回 std::error_code
的 code()
函数。同时,因为必须提供错误码,它既不提供空构造、也不提供接受字符串的构造函数。
从名字上就可以看出来(同时也是当时的提案 N2241 中指出的),整个 <system_error>
头文件都是为实现这个异常而生的。STL 有意将 std::error_code
作为 std::system_error
的附属品,而非单独的错误处理机制。
std::error_code
STL 中早期类似的错误码实现需求来自(C++17 才正式进入标准的)文件系统 API,当时的实现认为错误分类应在 errno
和操作系统原生错误码上二选一。但由于 STL 中网络、各种奇形怪状的 Boost 库的陆续加入,大家急需一个可扩展的错误码表示方案,才演变成现在的 std::error_code
。
std::error_code
顾名思义表示错误码,是由一个 int
型的 value
和一个 std::error_category *
型的 category
组成的值类型类。
之所以在 value
外还需要保存一个 category
,是因为即使是同样的错误码,在不同的库或者场景下表示的意义可能不同。同样是 42,在一个库的错误码中可能表示「文件不存在」,在另一个库中可能就表示「DNS 解析失败」。如果你熟悉 Cocoa 的那一套,那么这就类似 NSError
的 domain
属性。
此外,虽然没有明说 value
在等于 0 时表示无错误,但整个系统就是建立在这样的假设上的,例如:
operator bool
是根据value
是否非 0 来的clear()
函数会将value
设为 0
构造 std::error_code
std::error_code
的构造函数共有三种重载:
()
构造一个类似clear()
后的对象(value, category)
用参数填充对应的字段(enum_value)
调用make_error_code(enum_value)
工厂函数
前两种没啥好说的,第三种却值得推敲。
template<class ErrorCodeEnum>
error_code(ErrorCodeEnum e) noexcept;
此处的 ErrorCodeEnum
只是名字上说是枚举,但其实只要是用户定义类型就行(比如 enum class
/ enum
/ class
),所以理论上可以从异常直接构造 std::error_code
。
通常,如果构造函数只接受一个参数,那么我们推荐将它标记为 explicit
以免发生不必要的隐式转换。但这个函数不然,它要的就是让原始的 ErrorCodeEnum
可以隐式转换为 std::error_code
,从而实现这两者的直接比较。
catch (std::system_error const& e) {
if (e.code() == std::errc::invalid_argument) {
// blah blah blah...
}
}
当然,为了避免从任意类型的值都能搞个 std::error_code
出来,此重载只有当 std::is_error_code_enum<ErrorCodeEnum>::value
是 true
时才会有效。所以你自定义错误码枚举时,需要在 std
命名空间中特化此模版。
#include <iostream>
#include <system_error>
enum class YourErrorCode {
kSuccess = 0, // 别忘了 0 应该表示无错误
kNetworkError,
kBadRequest,
kServerError,
};
// 特化模版,启用对应的重载
namespace std {
template<>
struct is_error_code_enum<YourErrorCode>: true_type {};
}
// 提供工厂函数
// 工厂函数不必要写在 std 中
std::error_code make_error_code(YourErrorCode code)
{
return {
static_cast<int>(code),
std::generic_category(), // 这里暂时用自带的 category
};
}
int main()
{
std::error_code e = YourErrorCode::kBadRequest;
std::cout << e << '\n'; // 自带一个输出流的重载
}
当然,如果后续 C++ 中终于引入 Concept 的话,这个重载应该就没有这么复杂了吧。
std::error_category
前面说到 std::error_code
里保存的其实是指向 std::error_category
的指针,而非对象。这是因为这个类就是应该被当作单例来用的,实际也只能这么用才对,因为它的 operator=()
是通过直接比较 this
指针来实现的。
此外,std::error_category
还是个纯虚类,你必须实现的纯虚函数有:
name()
返回这类错误的名字message(int)
为给出的「错误码」返回对应的文字描述
STL 自带 std::error_category
的几个子类,很好地示范了「如何正确使用纯虚类」:不暴露具体的子类,而是暴露工厂函数,只通过接口(纯虚类)访问子类。
// 得到的都是 std::error_category const&
auto const& gec = std::generic_category();
auto const& sec = std::system_category();
std::error_condition
2007 年,<system_error>
正式进入标准前夕,委员会对 N2241 提案给出了一些意见,其中包括一条
Obscure the distinction between system-specific errors and the general and portable notion of an error condition.
于是,为了区分「系统相关的错误」和「平台无关的错误」,std::error_condition
诞生了。
因此你可以看到,std::error_condition
是一个与 std::error_code
除了语义几乎没有差别的东西。从库作者的角度,你可以理解为封装底层细节时用 std::error_code
,而对外暴露接口时推荐使用 std::error_condition
。
一起玩
std::error_condition
与 std::error_code
虽然是两个独立的类,但它们可以通过 std::error_category
连接在一起。
这两个类的对象互相比较时,STL 提供了对应的 operator=()
等函数的重载,通过调用双方的 category().equivalent(other)
来比较。只要任何一方的 category 认为对方与自己等价,两者就会被判为相等。
// 卖个萌 :-P
enum class MyErrorCondition {
kChenggong,
kWangluoCuowu,
kQingqiuCuowu,
kFuwuqiCuowu,
};
class MyErrorCategory: public std::error_category
{
public:
// 还记得 std::error_cateory 是个单例么?
static MyErrorCategory const& instance() {
static MyErrorCategory instance;
return instance;
}
char const *name() const noexcept override {
return "MyErrorCategory";
}
std::string message(int code) const override {
return "Message"; // 偷个懒
}
bool equivalent(std::error_code const& code, int condition) const noexcept override {
// 理论上你用不着在这里处理 code.category() == this->instance 的情况
// 因为是个单例,所以某些情况下不得不用这么绕的办法来拿
auto const& yourErrorCodeCategory = std::error_code(YourErrorCode{}).category();
if (code.category() == yourErrorCodeCategory) {
switch (static_cast<MyErrorCondition>(condition)) {
case MyErrorCondition::kChenggong:
return code == YourErrorCode::kSuccess;
case MyErrorCondition::kWangluoCuowu:
return code == YourErrorCode::kNetworkError;
case MyErrorCondition::kQingqiuCuowu:
return code == YourErrorCode::kBadRequest;
case MyErrorCondition::kFuwuqiCuowu:
return code == YourErrorCode::kServerError;
}
}
return false;
}
};
// error_condition 同样需要特化模版启动重载
namespace std {
template<>
struct is_error_condition_enum<MyErrorCondition>: true_type {};
}
// error_condition 同样可以通过工厂函数构造
std::error_condition make_error_condition(MyErrorCondition code)
{
return {static_cast<int>(code), MyErrorCategory::instance()};
}
int main()
{
std::error_code code = YourErrorCode::kNetworkError;
std::error_condition condition = MyErrorCondition::kWangluoCuowu;
std::cout << (code == condition) << '\n';
}
看上去有点绕,但由于通常 std::error_code
是底层接口定死的,而你所做的只能是用 std::error_condition
来适配的情况来说,还是很合理的。
还是 STL 轻松,直接抛 std::system_error
给你。std::error_code
甩脸,要抽象你自己去用 std::error_condition
搞,哈哈。
顺带一提
上面已经提到此处的 std::system_error
异常与 std::error_code
的关系。事实上,至少在提出引入 std::error_code
提案的作者看来,STL 乃至 C++ 中推荐的错误汇报方法还是异常。由于存在与操作系统等底层 API 的交互,才引入了「从 API 返回的错误码构建异常」以及「将异常转换为错误码传给 API」这样的需求。
当然,为了方便统一范式,便于不支持异常的系统使用文件系统 API。C++17 才正式进入标准的文件系统 API 也添加了「默认抛异常,但额外传一个 std::error_code
就不抛异常」的机制。
以上。
[...]std::error_code 和它的朋友们[...]