13 August 2023

C++ 核心指南目录

“If you can’t throw exceptions, use error codes systematically”

理由

比较系统的应用出错处理策略,可以减小忘记处理错误的风险。

请参考:模拟 RAII

注意

这里有几个问题需要澄清:

  • 你打算怎么把错误标记传递出函数?
  • 你打算怎么在函数退出的时候释放所有资源?
  • 你打算怎么使用错误标记?

一般来说,返回错误标记意味着返回两个值:函数结果和错误标记。错误标记可以是对象的一部分,比如某个对象有一个 valid() 函数,用来标记错误。或者返回成对的一对数值。

例子

Gadget make_gadget(int n)
{
    // ...
}

void user()
{
    Gadget g = make_gadget(17);
    if (!g.valid()) {
            // error handling
    }
    // ...
}

这个方法属于模拟 RAII 资源管理。 valid() 函数可以返回一个错误标记。错误标记可以是枚举类型的值。

例子

那么,如果我们不能或者不想要修改 Gadget 类型呢?这种情况,我们只能返回一对数值。

比如:

std::pair<Gadget, error_indicator> make_gadget(int n)
{
    // ...
}

void user()
{
    auto r = make_gadget(17);
    if (!r.second) {
            // error handling
    }
    Gadget& g = r.first;
    // ...
}

正如代码所展示的, std::pair 可以作为返回类型。有些人更喜欢自定义类型。比如:

Gval make_gadget(int n)
{
    // ...
}

void user()
{
    auto r = make_gadget(17);
    if (!r.err) {
            // error handling
    }
    Gadget& g = r.val;
    // ...
}

我们选择返回自定义类型是因为自定义类型可以给成员命名。这样就避免使用 std::pair 出现的混淆。像 first second 之类的名字太晦涩。

例子

一般来说,你必须因为错误而退出之前进行清理工作。可能会看起来很杂乱:

std::pair<int, error_indicator> user()
{
    Gadget g1 = make_gadget(17);
    if (!g1.valid()) {
        return {0, g1_error};
    }

    Gadget g2 = make_gadget(31);
    if (!g2.valid()) {
        cleanup(g1);
        return {0, g2_error};
    }

    // ...

    if (all_foobar(g1, g2)) {
        cleanup(g2);
        cleanup(g1);
        return {0, foobar_error};
    }

    // ...

    cleanup(g2);
    cleanup(g1);
    return {res, 0};
}

如果函数中有多个资源,多个出错点的时候,模拟 RAII 的工作量不小。一个常见的技术是把清理动作收集在一起放到函数后面,从而可以避免重复(注意, g2 的范围限定是不必要的,但是为了使得 goto 正确工作,需要做这个范围限定。

std::pair<int, error_indicator> user()
{
    error_indicator err = 0;
    int res = 0;

    Gadget g1 = make_gadget(17);
    if (!g1.valid()) {
        err = g1_error;
        goto g1_exit;
    }

    {
        Gadget g2 = make_gadget(31);
        if (!g2.valid()) {
            err = g2_error;
            goto g2_exit;
        }

        if (all_foobar(g1, g2)) {
            err = foobar_error;
            goto g2_exit;
        }

        // ...

    g2_exit:
        if (g2.valid()) cleanup(g2);
    }

g1_exit:
    if (g1.valid()) cleanup(g1);
    return {res, err};
}

函数越长,奇技淫巧越多。 finally 可以减轻一些痛苦。另外,程序越大,越难系统地实施这种基于错误标记的错误处理策略。

我们建议使用基于异常的错误处理,并建议保持函数精简短小

请查看:返回多个值