TimothyQiu's Blog

keep it simple stupid

std::error_code 和它的朋友们

分类:技术

前几天看 API 文档时候遇到了 std::error_code 这个东西,当时以为是 errno 的 Alias,后续查阅文档才发现并没有那么简单。

system_error

std::error_code 包含在 <system_error> 头文件中,相关的共有四个组件:

std::system_error 是一个继承自 std::runtime_error 的异常类,用以表示与 OS 交互时得到的以错误码形式返回的错误。因此除了提供标准的 what() 函数外,它还额外暴露了一个返回 std::error_codecode() 函数。同时,因为必须提供错误码,它既不提供空构造、也不提供接受字符串的构造函数。

从名字上就可以看出来(同时也是当时的提案 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 的那一套,那么这就类似 NSErrordomain 属性。

此外,虽然没有明说 value 在等于 0 时表示无错误,但整个系统就是建立在这样的假设上的,例如:

构造 std::error_code

std::error_code 的构造函数共有三种重载:

前两种没啥好说的,第三种却值得推敲。

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>::valuetrue 时才会有效。所以你自定义错误码枚举时,需要在 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 还是个纯虚类,你必须实现的纯虚函数有:

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_conditionstd::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 就不抛异常」的机制。

以上。

C++

添加新评论 »