自定义 C 语言错误代码 Error Codes

发布于 2024-12-15 16:53:19 字数 11124 浏览 2 评论 0

许多 C 库遵循 POSIX 传统 ,以整数形式返回错误。一些库重用预定义的 errno 值,而其他人则定义自己的图书馆特定值。

我们展示了如何将此类 C 库的错误值与 std::error_code 集成。选择 std::error_code 给我们带来了几个优点:(i) 错误可以被类型擦除,(ii) 错误可以在不丢失信息的情况下传播,(iii) 错误是可互操作的。

Type-erased 错误

error_code

std::error_code 包含两个成员变量:一个 int 类型的错误值和一个指向代表错误类型的库特定类别。该类别是一个单例,继承自 std::error_category< /代码>

每个库必须定义自己的错误类别,该类别会覆盖 name()message()std::error_category 的 code> 函数。前者返回类别的名称,后者返回具体错误值的文本表示。该类别可以被视为多态 std::type_info 必须手动实现,因此无需 RTTI 即可工作。

如果两个 std::error_code 对象具有相同的错误值并且指向相同的类别单例,则它们相等。

std::error_code 是类型擦除的,这意味着无论它嵌入哪种错误类型,它的类型都保持不变。这是因为错误类别是多态单例,并且因为 std::error_code 存储类别单例的地址,所以没有 对象切片 将在复制 std::error_code 时发生。多态性在这里用作类型擦除技术。

类型擦除给我们带来了一些优势:

  1. 类型擦除使我们能够构建 编译防火墙 ,从而我们的 C++ 头文件不要意外地暴露底层 C 库中的宏和全局符号。
  2. 无损传播意味着虽然错误被类型擦除,但我们保留了 C 库中的原始错误值及其类型。类型不是直接保存的,而是可以从类别中推断出来。如果 C 库的升级引入了新的错误值,即使我们不升级 C++ 适配器,它们也会传播。
  3. 不同类别的错误可以相互比较,因为错误类别具有等效的概念。等价性使得添加新的 C 库变得更加容易,如下所示。

音频编解码器示例

这里运行的示例是用于 音频编解码器 库的 C++ 适配器。

这些库对于我们的目的特别有趣,因为尽管它们实现了不同的音频编解码器,并且由不同的开发人员编写,但它们的错误值共享一个 家族相似性 – 它们的错误值之间有足够的重叠,因此可以以相同的方式处理它们。例如,所有音频编解码器都支持有限的采样率,如果调用者选择不受支持的采样率,它们会返回错误。

同时,也存在差异,因此我们无法将错误值转换为预定义的常见错误集。因此,我们希望在不丢失信息的情况下传播实际误差值,但我们只想对众所周知的错误进行比较和反应。

std::error_code 最初设计的目的是允许多个子系统将自己的错误值封装在一个公共错误容器中。因此,我们可以使用 std::error_code 从所有音频编解码器库传播错误值。我们只需为每个库定义一个错误类别。

此外,音频解码器与向其提供数据的传输机制配对 - 例如文件读取器或网络流 - 因此我们也必须能够通过传输错误。我们假设来自传输层的错误已经包含在 std::error_code 中。如果没有,可以使用下面描述的技术。

本机适配器

mpg123 适配器 图 2 :mpg123 的本机适配器。

假设我们要为 mpg123 音频编解码器库创建一个 C++ 适配器。以下是其错误值的选择性部分。

// mpg123.h

enum mpg123_errors
{
  MPG123_OK = 0,
  MPG123_BAD_RATE = 3,
  MPG123_BAD_PARAM = 5,
  MPG123_OUT_OF_MEMORY = 7,
  // ...
};

const char *mpg123_plain_strerror(int);

mpg123_plain_strerror() 函数将错误值转换为文本表示形式。

C++ 适配器仅由一个错误类别和一个 make_error_code() 函数组成,该函数用于将 mpg123 错误值转换为 make_error_code() 函数-rouge">std::error_code 。

// mpg123.hpp

#include <system_error>

namespace mpg123
{

const std::error_category& category();

inline std::error_code make_error_code(int mpg123_value)
{
  // Create an error_code with the original mpg123 error value
  // and the mpg123 error category.
  return std::error_code(mpg123_value, mpg123::category());
}

} // namespace mpg123

请注意,此标头不包含 C 库标头 ,因此我们没有使用 C 库中的符号污染全局命名空间。

大部分实现都是样板代码,其主要目的是将类别 message() 调用委托给 mpg123_plain_strerror() 函数。

// mpg123.cpp

#include <mpg123.h> // C header
#include <mpg123.hpp> // Adapter

namespace mpg123
{
namespace detail
{

class category : public std::error_category
{
public:
  virtual const char *name() const noexcept override
  {
    return "mpg123::category";
  }
  virtual std::string message(int value) const override
  {
    // Let the native function do the actual work
    return ::mpg123_plain_strerror(value);
  }
};

} // namespace detail

const std::error_category& category()
{
  // The category singleton
  static detail::category instance;
  return instance;
}

} // namespace mpg123

我们可以以类似的方式为其他音频编解码器库定义适配器。

使用适配器

每当 C++ 适配器遇到来自 mpg123 库的错误时,我们可以使用 mpg123::make_error_code() 返回一个 std::error_code 给调用者。

  // ...
  int error = mpg123_getformat(handle, &rate, &channels, &encoding);
  if (error != MPG123_OK)
    return mpg123::make_error_code(error);

然后调用者有几个选择:

  1. 打印错误。这是通过 error_code::message() 完成的。
  2. 抛出错误。将 error_code 包装在 std::system_error 异常。更好的是,定义一个继承自 std::system_errormpg123::error 异常> ,因此来自 m​​pg123 适配器的异常可以与 try-catch 块中的其他 system_error 异常区分开来。
  3. 检查错误值。由于 error_code 包含原始错误值,因此调用者必须首先检查错误代码是否属于 mpg123::category 然后将 error_code::value()mpg123_errors 枚举器在 中定义。

如果比较是在 .cpp 文件中完成的,则上述所有选项都可以在编译防火墙后面完成。

// caller.cpp

#include <mpg123.h>
#include <mpg123.hpp>

void inform(const std::error_code& error)
{
  if (error.category() == mpg123::category())
  {
    switch (error.value())
    {
    case MPG123_OK:
      break; // Nothing to report
    case MPG123_BAD_RATE:
      inform_bad_format();
      break;
    case MPG123_BAD_PARAM:
      inform_illegal_argument();
      break;
    case MPG123_OUT_OF_MEMORY:
      std::terminate();
    }
  }
}

这是一种可管理的解决方案,尽管不是一个优雅的解决方案。当我们添加另一个音频编解码器时,例如 libopus ,那么调用者也必须检查其错误值。

// caller.cpp

#include <mpg123.h>
#include <opus/opus.h>
#include <mpg123.hpp>
#include <opus.hpp>

void inform(const std::error_code& error)
{
  if (error.category() == mpg123::category())
  {
    switch (error.value())
    {
    case MPG123_OK:
      break; // Nothing to report
    case MPG123_BAD_RATE:
      inform_bad_format();
      break;
    case MPG123_BAD_PARAM:
      inform_illegal_argument();
      break;
    case MPG123_OUT_OF_MEMORY:
      std::terminate();
    }
  }
  else if (error.category() == opus::category())
  {
    switch (error.value())
    {
    case OPUS_OK:
      break; // Nothing to report
    case OPUS_BAD_ARG:
      inform_illegal_argument();
      break;
    case OPUS_INVALID_PACKET:
      inform_bad_format();
    case OPUS_ALLOC_FAIL:
      std::terminate();
    }
  }
}

尽管我们的打印和抛出用例非常简单,但检查用例给我们的 C++ 适配器的用户带来了沉重的负担,并且我们包装的每个音频编解码器的负担都会增加。我们需要更好的东西。

通用枚举

通用适配器 图 3 :带有枚举器的通用适配器。

检查错误的一种更用户友好的方法是定义要比较的枚举器。我们注意到一些本机错误值已被 覆盖 std::errc ,例如 invalid_argument ,因此我们不会将它们包含在我们自己的枚举中。

调用者应该能够编写以下内容。请注意,省略了 C 库 标头。这意味着我们维护编译防火墙。

// caller2.cpp

#include <codec.hpp>

void inform(const std::error_code& error)
{
  if (error == codec::illegal_sample_rate)
    inform_bad_format();
  else if (error == std::errc::invalid_argument)
    inform_invalid_argument();
  else if (error == std::errc::not_enough_memory)
    std::terminate();
}

我们定义了一个通用枚举和错误类别,我们希望在所有音频编解码器库中使用它们。

// codec.hpp

#include <system_error>

namespace codec
{

enum class errc
{
  success = 0,
  illegal_sample_rate,
  illegal_sample_width
};

const std::error_category& category();
std::error_code make_error_code(codec::errc);
std::error_condition make_error_condition(codec::errc);

} // namespace codec

由于本机错误值和 codec::errc 枚举具有不同的类型和数值,因此我们需要它们之间的映射。这种映射是由我们类别的 equivalent() 成员函数完成的。

// mpg123.cpp

#include <mpg123.h>
#include <codec.hpp>
#include <mpg123.hpp>

namespace mpg123
{
namespace detail
{

class category : public std::error_category
{
public:

  // ...

  // Compare own value with foreign condition
  virtual bool equivalent(
    int mpg123_value,
    const std::error_condition& condition) const noexcept override
  {
      switch (mpg123_value)
      {
      case MPG123_OK:
          return bool(condition);
      case MPG123_BAD_RATE:
          return condition ==
            codec::make_error_condition(codec::errc::illegal_sample_rate);
      case MPG123_BAD_PARAM:
          return condition ==
            std::make_error_condition(std::errc::illegal_argument);
      case MPG123_OUT_OF_MEMORY:
          return condition ==
            std::make_error_condition(std::errc::not_enough_memory);
      default:
          return false;
      }
  }
};

} // namespace detail

// ...

} // namespace mpg123

最后,我们必须创建 std:: is_error_condition_enum 因此我们的枚举可以直接与 std::error_code 进行比较。

// codec.hpp

namespace std
{

template <>
struct is_error_condition_enum<codec::errc>
  : public std::true_type {};

} // namespace std

上述的优点是我们已将 mpg123 库的所有知识移回到 C++ 适配器中。

错误条件

我们使用了 std::error_condition 在 C++ 适配器中的比较中。我们不能使用 std::error_code 因为它将执行精确匹配。相反,引入了 std::error_condition 来实现等效比较。

std::error_condition 的描述有点模糊。 C++ 标准 syserr.errcondition.overview 指出

类 error_condition 描述了一个用于保存标识错误条件的值的对象。注意:error_condition 值是可移植的抽象,而 error_code 值 (19.5.3) 是特定于实现的。 ——尾注

没有详细说明错误条件是什么。这给我们留下了这样的印象:std::error_condition 可用于传播与平台无关的错误。这是因为它的界面几乎与 std::error_code 相同。

事实并非如此。正如原始设计者之一 解释

class error_condition - 您想要测试并可能在代码中做出反应的东西。

因此,更有用的指南是:

  • 使用 std::error_code 进行错误传播。
  • 使用 std::error_code 在类别内进行比较。
  • 使用 std::error_condition 进行类别之间的比较。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

浅忆

暂无简介

0 文章
0 评论
23 人气
更多

推荐作者

已经忘了多久

文章 0 评论 0

15867725375

文章 0 评论 0

LonelySnow

文章 0 评论 0

走过海棠暮

文章 0 评论 0

轻许诺言

文章 0 评论 0

信馬由缰

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文