使用`std::error_code`的最佳实践

5

我目前正在构建一个嵌入式系统,并使用现代C++编译器。虽然从技术上讲,我可以在给定的资源(ARM7,超过10M RAM)中适配异常处理,但我认为异常不是这种情况下的合适工具,并且使用异常需要RTTI,这反过来会导致代码膨胀。

为了保持C++风格,我想要使用std::error_code(或类似带有更多数据的内容),因为我喜欢这个概念。

但是,似乎没有任何共识可以告诉我如何实际使用它们。我至少看到了四种不同的在函数调用之间传递它们的方法,其中两种具有多个语义。

  1. Passing by pointer as an argument

    void somefunction(Args..., std::error_code* error);
    

    This is the way I have not seen that often and the one I dislike the most. It leaves the return type fully available and (often, but not always) passing nullptr resulted in normal throwing behaviour.

  2. Passing by reference as an argument

    void somefunction(Args..., std::error_code& error);
    

    This is the one I prefer. It leaves returnvalue fully available and makes clear that the error_code is not optional.

  3. Returning it by value

    std::error_code somefunction(Ret& out <= if used, Args...);
    

    I have seen this one quite often but don’t really like it that much, as it uses up your return value and I generally don’t like “out parameters” unless there’s no way around them.

  4. Returning a std::variant<Ret, std::error_code>

    std::variant<Ret, std::error_code> somefunction(Args...);
    

    This one allows for a return value, but makes accessing both value and error harder. Also, it makes code calling the function more verbose.

语义学

我看到了两种不同的语义方式,如果传递了error_code参数:

  • 在开始时清除并在出现错误时设置
  • 仅在出现错误时设置
  • 如果 error_code 被“设置”,则立即返回正确值

如果您想减少调用代码中的错误检查,则最后一种方法非常好。因为您可以只传递一个error_code参数给多个函数,而无需在它们之间进行检查;在出现第一个错误后,其余的所有内容都将不会执行,类似于异常处理。

我个人更喜欢第二种带检查和返回的方式,但我可能有偏见。

是否有一些建议或通常接受的做法呢?


你好。也许有点离题,但我会使用异常。你可以“自定义”它们传输自己的错误代码,对函数原型没有“影响”,并且如果有人忘记检查“返回”的错误代码,代码将在开发阶段崩溃。调用者代码也更清晰,因为你可以在不必在每个函数调用位置测试返回的错误代码的情况下,在快乐路径(无错误)和异常处理路径之间进行分割。 - Jean-Marc Volle
问题在于我可能需要将应用程序的部分或全部移植到低规格平台上,这种情况下可能没有足够的空间来容纳异常版本(它会添加大量用于异常处理和rtti的代码)。 - Thalhammer
我认为指针是这个例子中应该采用的方法,虽然通过引用也不错,但这只是我的偏见,我并不认为有对与错之分。 - just a guy
为什么异常处理需要运行时类型识别(RTTI)?我认为它们是独立的属性。 - Mark Ransom
@MarkRansom 因为你可能会抛出 std::runtime_error 并捕获 const std::exception&。编译器(或者准确来说是异常实现)需要一种方法来确定它们是否匹配,使用 rtti(类似于 dynamic_cast 的方式)。你可以禁用 rtti 并使用异常,但这只会禁用前端,实际的 rtti 信息和后端函数仍然会被发出。 - Thalhammer
1个回答

2

好的,这不是一个完整的答案,实际上也不是完全相关的主题,因为我不知道有没有标准的方法来做到这一点。但是我曾经看到一个巧妙的小技巧,可以使错误代码更难被滥用。考虑以下代码:

结构体 MyEC {
    MyEC() {}
    MyEC(MyEC && other) : parent(&other) {
        // 如果未检查 other,则可能记录日志和/或中止
        other.checked = false;
    }
    // 删除其他构造函数和赋值运算符
~MyEC() { if(!checked && parent == nullptr) { // 记录日志和/或中止 } }
[[nodiscard]] std::error_code check() { checked = true; return ec; } void set(std::error_code err) { if(parent == nullptr) ec = err; else parent->set(err); } private: MyEC* parent = nullptr; checked = true; std::error_code ec {}; };
int foo(MyEC && err) { err.set(/* some error */); return 5; }
int foo1(MyEC &&) { return 4; }
void bar() { MyEC err; foo(std::move(err)); // 如果未检查 err,则 err 具有错误代码,我们将知道
foo1(std::move(err)); // 即使没有错误发生,如果未检查 err,我们也会中止。 }

即使错误代码未设置但也未检查,它仍会中止,这非常好。在移动后它有很多用途,这有点奇怪,但在这里没有问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接