Boost C++ 库

...one of the most highly regarded and expertly designed C++ library projects in the world. Herb SutterAndrei Alexandrescu, C++ 编码标准

摘要

Boost LEAF 是一个用于 C++11 的轻量级错误处理库。特性

  • 可移植的单头文件格式,无依赖项。

  • 代码体积小,可配置用于嵌入式开发。

  • 无动态内存分配,即使对于非常大的负载也是如此。

  • 在“happy”路径和“sad”路径上具有确定性的无偏差效率。

  • 错误对象以恒定时间处理,与调用堆栈深度无关。

  • 可以与异常处理一起使用,也可以不使用。

教程 | 概要 | 白皮书

参考: 函数 | 类型 | 谓词 | 特性 |

支持

分发

LEAF 根据 Boost 软件许可,版本 1.0 分发。

有三个分发渠道

  • LEAF 包含在官方 Boost 版本中(从 Boost 1.75 开始),因此可以通过大多数软件包管理器获得。

  • 源代码托管在 GitHub 上。

  • 为了最大限度的可移植性,最新的 LEAF 版本也以单头文件格式提供:leaf.hpp(直接下载链接)。

LEAF 不依赖于 Boost 或其他库。

教程

什么是失败?它仅仅是函数无法返回有效结果,而是生成一个描述失败原因的错误对象。

一种典型的设计是返回变体类型,例如 result<T, E>。在内部,此类变体类型必须存储一个判别式(在本例中为布尔值),以指示对象是包含 T 还是 E

LEAF 的设计基于这样的观察:直接调用者必须访问判别式才能确定有效 T 的可用性,但在其他情况下,它很少需要访问任何错误对象。只有当到达错误处理范围时才需要它们。

因此,原本是 result<T, E> 的东西变成了 result<T>,它存储判别式和(可选的)T,而错误对象直接传递到需要它们的错误处理范围。

这种分解的好处是 result<T> 变得非常轻量级,因为它不与错误类型耦合;此外,错误对象以恒定时间(与调用堆栈深度无关)进行通信。即使是非常大的对象也可以在没有动态内存分配的情况下高效处理。

报告错误

报告错误的函数

enum class err1 { e1, e2, e3 };

leaf::result<T> f()
{
  ....
  if( error_detected )
    return leaf::new_error( err1::e1 ); // Pass an error object of any type

  // Produce and return a T.
}

检查错误

检查 leaf::result<T> 传达的错误,其工作方式符合预期

leaf::result<U> g()
{
  leaf::result<T> r = f();
  if( !r )
    return r.error();

  T const & v = r.value();
  // Use v to produce a valid U
}
r.error() 的结果与 leaf::result 模板的任何实例兼容。在上面的示例中,请注意 g 返回 leaf::result<U>,而 r 的类型为 leaf::result<T>

可以使用 BOOST_LEAF_AUTO 避免样板 if 语句

leaf::result<U> g()
{
  BOOST_LEAF_AUTO(v, f()); // Bail out on error

  // Use v to produce a valid U
}

BOOST_LEAF_AUTO 不能用于 void 结果;在这种情况下,为了避免样板 if 语句,请使用 BOOST_LEAF_CHECK

leaf::result<void> f();

leaf::result<int> g()
{
  BOOST_LEAF_CHECK(f()); // Bail out on error
  return 42;
}

在定义了 __GNUC__ 的实现(例如 GCC/clang)上,BOOST_LEAF_CHECK 宏定义利用了 GNU C 语句表达式。在这种情况下,除了其与 result<void> 的可移植用法之外,BOOST_LEAF_CHECK 还可以用于具有非 void 结果类型的表达式中

leaf::result<int> f();

float g(int x);

leaf::result<float> t()
{
  return g( BOOST_LEAF_CHECK(f()) );
}

以下是可移植的替代方案

leaf::result<float> t()
{
  BOOST_LEAF_AUTO(x, f());
  return g(x);
}

错误处理

错误处理范围必须使用特殊语法来指示它们需要访问错误对象。以下摘录尝试了几个操作并处理了 err1 类型的错误

leaf::result<U> r = leaf::try_handle_some(

  []() -> leaf::result<U>
  {
    BOOST_LEAF_AUTO(v1, f1());
    BOOST_LEAF_AUTO(v2, f2());

    return g(v1, v2);
  },

  []( err1 e ) -> leaf::result<U>
  {
    if( e == err1::e1 )
      .... // Handle err1::e1
    else
      .... // Handle any other err1 value
  } );

首先,try_handle_some 执行传递给它的第一个函数;它尝试生成 result<U>,但可能会失败。

第二个 lambda 是错误处理程序:如果第一个 lambda 失败并出现 err1 类型的错误对象,则将调用它。该对象存储在堆栈上,本地于 try_handle_some 函数(LEAF 知道分配此存储,因为我们给它提供了一个接受 err1 的错误处理程序)。传递给 leaf::try_handle_some 的错误处理程序可以返回有效的 leaf::result<U>,但允许失败。

错误处理程序可以声明它只能处理给定错误类型的某些特定值

leaf::result<U> r = leaf::try_handle_some(

  []() -> leaf::result<U>
  {
    BOOST_LEAF_AUTO(v1, f1());
    BOOST_LEAF_AUTO(v2, f2());

    return g(v1. v2);
  },

  []( leaf::match<err1, err1::e1, err1::e3> ) -> leaf::result<U>
  {
    // Handle err1::e1 or err1::e3
  },

  []( err1 e ) -> leaf::result<U>
  {
    // Handle any other err1 value
  } );

LEAF 按顺序考虑提供的错误处理程序,并根据当前正在通信的错误对象调用它能够为其提供参数的第一个处理程序。以上

  • 如果 err1 类型的错误对象可用,并且其值为 err1::e1err1::e3,则将调用第一个错误处理程序。

  • 否则,如果 err1 类型的错误对象可用(无论其值如何),则将调用第二个错误处理程序。

  • 否则,leaf::try_handle_some 无法处理该错误。

错误处理程序可能会有条件地使故障保持未处理状态

leaf::result<U> r = leaf::try_handle_some(

  []() -> leaf::result<U>
  {
    BOOST_LEAF_AUTO(v1, f1());
    BOOST_LEAF_AUTO(v2, f2());

    return g(v1. v2);
  },

  []( err1 e, leaf::error_info const & ei ) -> leaf::result<U>
  {
    if( <<condition>> )
      return valid_U;
    else
      return ei.error();
  } );

任何错误处理程序都可以接受 leaf::error_info const & 类型的参数,以访问有关正在处理的错误的通用信息;在本例中,我们使用 error 成员函数,该函数返回当前错误的唯一 error_id;我们使用它来初始化返回的 leaf::result,从而有效地将当前错误传播出 try_handle_some

如果我们想发出新错误的信号(而不是传播当前错误),则在 return 语句中,我们将调用 leaf::new_error 函数。

如果我们想确保处理所有可能的故障,我们使用 leaf::try_handle_all 而不是 leaf::try_handle_some

U r = leaf::try_handle_all(

  []() -> leaf::result<U>
  {
    BOOST_LEAF_AUTO(v1, f1());
    BOOST_LEAF_AUTO(v2, f2());

    return g(v1. v2);
  },

  []( leaf::match<err1, err1::e1> ) -> U
  {
    // Handle err::e1
  },

  []( err1 e ) -> U
  {
    // Handle any other err1 value
  },

  []() -> U
  {
    // Handle any other failure
  } );

leaf::try_handle_all 函数在编译时强制执行:至少一个提供的错误处理程序不带任何参数(因此能够处理任何故障)。此外,所有错误处理程序都被强制返回有效的 U,而不是 leaf::result<U>,以便保证 leaf::try_handle_all 始终成功。


使用不同的错误类型

当然,可以为不同的错误类型提供不同的处理程序

enum class err1 { e1, e2, e3 };
enum class err2 { e1, e2 };

....

leaf::result<U> r = leaf::try_handle_some(

  []() -> leaf::result<U>
  {
    BOOST_LEAF_AUTO(v1, f1());
    BOOST_LEAF_AUTO(v2, f2());

    return g(v1, v2);
  },

  []( err1 e ) -> leaf::result<U>
  {
    // Handle errors of type `err1`.
  },

  []( err2 e ) -> leaf::result<U>
  {
    // Handle errors of type `err2`.
  } );

错误处理程序始终按顺序考虑

  • 如果 err1 类型的错误对象可用,则将使用第一个错误处理程序;

  • 否则,如果 err2 类型的错误对象可用,则将使用第二个错误处理程序;

  • 否则,leaf::try_handle_some 将失败。


使用多个错误对象

可以调用带有多个错误对象的 leaf::new_error 函数,例如,用于传达错误代码和相关文件名

enum class io_error { open_error, read_error, write_error };

struct e_file_name { std::string value; }

leaf::result<File> open_file( char const * name )
{
  ....
  if( open_failed )
    return leaf::new_error(io_error::open_error, e_file_name {name});
  ....
}

同样,错误处理程序可以将多个错误对象作为参数

leaf::result<U> r = leaf::try_handle_some(

  []() -> leaf::result<U>
  {
    BOOST_LEAF_AUTO(f, open_file(fn));
    ....
  },

  []( io_error ec, e_file_name fn ) -> leaf::result<U>
  {
    // Handle I/O errors when a file name is also available.
  },

  []( io_error ec ) -> leaf::result<U>
  {
    // Handle I/O errors when no file name is available.
  } );

再次强调,错误处理程序按顺序考虑

  • 如果 io_error 类型的错误对象 e_file_name 类型的错误对象都可用,则将使用第一个错误处理程序;

  • 否则,如果 io_error 类型的错误对象可用,则将使用第二个错误处理程序;

  • 否则,leaf_try_handle_some 将失败。

编写上述代码的另一种方法是提供一个错误处理程序,该处理程序将 e_file_name 参数作为指针

leaf::result<U> r = leaf::try_handle_some(

  []() -> leaf::result<U>
  {
    BOOST_LEAF_AUTO(f, open_file(fn));
    ....
  },

  []( io_error ec, e_file_name const * fn ) -> leaf::result<U>
  {
    if( fn )
      .... // Handle I/O errors when a file name is also available.
    else
      .... // Handle I/O errors when no file name is available.
  } );

永远不会因为缺少处理程序作为指针的类型的错误对象而删除错误处理程序;在这种情况下,LEAF 只需为这些参数传递 nullptr

当错误处理程序通过可变引用或指针获取参数时,当错误传递给调用方时,对其状态的更改将保留。

增强错误

假设我们有一个函数 parse_line,它可能因 io_errorparse_error 而失败

enum class io_error { open_error, read_error, write_error };

enum class parse_error { bad_syntax, bad_range };

leaf::result<int> parse_line( FILE * f );

leaf::on_error 函数可用于自动将其他错误对象与“正在处理”的任何故障关联起来

struct e_line { int value; };

leaf::result<void> process_file( FILE * f )
{
  for( int current_line = 1; current_line != 10; ++current_line )
  {
    auto load = leaf::on_error( e_line {current_line} );

    BOOST_LEAF_AUTO(v, parse_line(f));

    // use v
  }
}

由于 process_file 不处理错误,因此它对故障保持中立,除非在出现问题时附加 current_lineon_error 返回的对象包含包装在 struct e_line 中的 current_line 的副本。如果 parse_line 成功,则 e_line 对象将被简单地丢弃;如果失败,则 e_line 对象将自动“附加”到故障。

然后可以像这样处理此类故障

leaf::result<void> r = leaf::try_handle_some(

  [&]() -> leaf::result<void>
  {
    BOOST_LEAF_CHECK( process_file(f) );
  },

  []( parse_error e, e_line current_line  )
  {
    std::cerr << "Parse error at line " << current_line.value << std::endl;
  },

  []( io_error e, e_line current_line )
  {
    std::cerr << "I/O error at line " << current_line.value << std::endl;
  },

  []( io_error e )
  {
    std::cerr << "I/O error" << std::endl;
  } );

以下是等效的,可能更简单

leaf::result<void> r = leaf::try_handle_some(

  []() -> leaf::result<void>
  {
    BOOST_LEAF_CHECK( process_file(f) );
  },

  []( parse_error e, e_line current_line )
  {
    std::cerr << "Parse error at line " << current_line.value << std::endl;
  },

  []( io_error e, e_line const * current_line )
  {
    std::cerr << "Parse error";
    if( current_line )
      std::cerr << " at line " << current_line->value;
    std::cerr << std::endl;
  } );

异常处理

如果操作抛出异常会发生什么?try_handle_sometry_handle_all 都会捕获异常,并且能够将它们传递给任何兼容的错误处理程序

leaf::result<void> r = leaf::try_handle_some(

  []() -> leaf::result<void>
  {
    BOOST_LEAF_CHECK( process_file(f) );
  },

  []( std::bad_alloc const & )
  {
    std::cerr << "Out of memory!" << std::endl;
  },

  []( parse_error e, e_line l )
  {
    std::cerr << "Parse error at line " << l.value << std::endl;
  },

  []( io_error e, e_line const * l )
  {
    std::cerr << "Parse error";
    if( l )
      std::cerr << " at line " << l.value;
    std::cerr << std::endl;
  } );

上面,我们只是添加了一个接受 std::bad_alloc 的错误处理程序,一切都“按预期工作”:无论故障是通过 leaf::result 还是通过异常传递,LEAF 都会正确调度错误处理程序。

当然,如果我们专门使用异常处理,则根本不需要 leaf::result。在这种情况下,我们使用 leaf::try_catch

leaf::try_catch(

  []
  {
    process_file(f);
  },

  []( std::bad_alloc const & )
  {
    std::cerr << "Out of memory!" << std::endl;
  },

  []( parse_error e, e_line l )
  {
    std::cerr << "Parse error at line " << l.value << std::endl;
  },

  []( io_error e, e_line const * l )
  {
    std::cerr << "Parse error";
    if( l )
      std::cerr << " at line " << l.value;
    std::cerr << std::endl;
  } );

我们不必更改错误处理程序!但这如何工作?process_file 抛出什么样的异常?

LEAF 启用了一种新颖的异常处理技术,该技术不需要异常类型层次结构来对故障进行分类,也不在异常对象中携带数据。回想一下,当故障通过 leaf::result 传递时,我们在 return 语句中调用 leaf::new_error,传递任意数量的错误对象,这些对象直接发送到正确的错误处理范围

enum class err1 { e1, e2, e3 };
enum class err2 { e1, e2 };

....

leaf::result<T> f()
{
  ....
  if( error_detected )
    return leaf::new_error(err1::e1, err2::e2);

  // Produce and return a T.
}

当使用异常处理时,这变为

enum class err1 { e1, e2, e3 };
enum class err2 { e1, e2 };

T f()
{
  if( error_detected )
    leaf::throw_exception(err1::e1, err2::e2);

  // Produce and return a T.
}

leaf::throw_exception 函数像 leaf::new_error 一样处理传递的错误对象,然后抛出一个类型派生自 std::exception 的对象。使用此技术,异常类型并不重要:leaf::try_catch 捕获所有异常,然后执行通常的 LEAF 错误处理程序选择例程。

相反,如果我们想使用抛出不同类型来指示不同故障的常用约定,我们只需将异常对象(即类型派生自 std::exception 的对象)作为第一个参数传递给 leaf::throw_exception

leaf::throw_exception(std::runtime_error("Error!"), err1::e1, err2::e2);

在这种情况下,抛出的异常对象将是类型派生自 std::runtime_error,而不是 std::exception

最后,leaf::on_error 也“可以正常工作”。这是我们的 process_file 函数,经过重写以使用异常而不是返回 leaf::result(请参阅 增强错误

int parse_line( FILE * f ); // Throws

struct e_line { int value; };

void process_file( FILE * f )
{
  for( int current_line = 1; current_line != 10; ++current_line )
  {
    auto load = leaf::on_error( e_line {current_line} );
    int v = parse_line(f);

    // use v
  }
}

使用外部 result 类型

静态类型检查在任何重要的项目中的错误处理互操作性方面都会造成困难。使用异常处理在某种程度上缓解了这个问题,因为在这种情况下,错误类型不会被烧录到函数签名中,因此错误很容易穿透多层 API;但这通常对 C++ 没有帮助,因为社区在异常处理问题上存在分歧。尽管存在争议,但现实情况是 C++ 程序需要通过多层 API 处理通过大量错误代码、result 类型和异常传递的错误。

LEAF 使应用程序开发人员能够从每个单独库的 result 类型中抖出错误对象,并将它们逐字发送到错误处理范围。这是一个示例

lib1::result<int, lib1::error_code> foo();
lib2::result<int, lib2::error_code> bar();

int g( int a, int b );

leaf::result<int> f()
{
  auto a = foo();
  if( !a )
    return leaf::new_error( a.error() );

  auto b = bar();
  if( !b )
    return leaf::new_error( b.error() );

  return g( a.value(), b.value() );
}

稍后我们只需调用 leaf::try_handle_some,并为每种类型传递一个错误处理程序

leaf::result<int> r = leaf::try_handle_some(

  []() -> leaf::result<int>
  {
    return f();
  },

  []( lib1::error_code ec ) -> leaf::result<int>
  {
    // Handle lib1::error_code
  },

  []( lib2::error_code ec ) -> leaf::result<int>
  {
    // Handle lib2::error_code
  } );
}

一个可能的复杂情况是,我们可能无法选择从 f 返回 leaf::result<int>:第三方 API 可能会对其施加特定的签名,强制其返回库特定的 result 类型。当 f 旨在用作回调时,情况就是如此

void register_callback( std::function<lib3::result<int>()> const & callback );

在这种情况下我们可以使用 LEAF 吗?实际上,只要 lib3::result 能够传递 std::error_code,我们就可以使用。我们只需要通过专门化 is_result_type 模板来告知 LEAF

namespace boost { namespace leaf {

template <class T>
struct is_result_type<lib3::result<T>>: std::true_type;

} }

有了这个,即使 lib3::result 无法传输 lib1 错误或 lib2 错误,f 也能像以前一样工作

lib1::result<int, lib1::error_type> foo();
lib2::result<int, lib2::error_type> bar();

int g( int a, int b );

lib3::result<int> f() // Note: return type is not leaf::result<int>
{
  auto a = foo();
  if( !a )
    return leaf::new_error( a.error() );

  auto b = bar();
  if( !b )
    return leaf::new_error( b.error() );

  return g( a.value(), b.value() );
}

leaf::new_error 返回的对象使用 LEAF 特定的 error_category 隐式转换为 std::error_code,这使得 lib3::resultleaf::try_handle_some(以及 leaf::try_handle_all)兼容

lib3::result<int> r = leaf::try_handle_some(

  []() -> lib3::result<int>
  {
    return f();
  },

  []( lib1::error_code ec ) -> lib3::result<int>
  {
    // Handle lib1::error_code
  },

  []( lib2::error_code ec ) -> lib3::result<int>
  {
    // Handle lib2::error_code
  } );
}

互操作性

理想情况下,当检测到错误时,使用 LEAF 的程序始终会调用 new_error,确保遇到的每个故障都绝对分配一个唯一的 error_id,然后通过异常或 result<T> 对象可靠地传递到适当的错误处理范围。

唉,这并非总是可能的。

例如,错误可能需要通过不合作的第三方接口进行传递。为了方便这种传输,错误 ID 可以编码在 std::error_code 中。只要第三方接口能够传输 std::error_code,它就可以与 LEAF 兼容。

此外,有时需要通过甚至不使用 std::error_code 的接口来传递错误。一个例子是当外部底层库抛出异常时,该异常不太可能携带 error_id

为了支持这种棘手的用例,LEAF 提供了函数 current_error,该函数返回(从此线程)最近一次调用 new_error 返回的错误 ID。解决问题的一种可能方法是使用以下逻辑(由 error_monitor 类型实现)

  1. 在调用不合作的 API 之前,调用 current_error 并缓存返回的值。

  2. 调用 API,然后再次调用 current_error

    1. 如果这返回的值与之前相同,则将错误对象传递给 new_error,以将它们与新的 error_id 关联起来;

    2. 否则,将错误对象与第二次调用 current_error 返回的 error_id 值关联起来。

请注意,如果上述逻辑是嵌套的(例如,一个函数调用另一个函数),则 new_error 将仅由最内层的函数调用,因为该调用保证所有调用函数都将命中 else 分支。

有关详细教程,请参阅 使用 error_monitor 从 C 回调报告任意错误


加载错误对象

回想一下,传递给 LEAF 的错误对象存储在堆栈上,本地于用于处理错误的 try_handle_sametry_handle_alltry_catch 函数。加载错误对象意味着将其移动到此类存储中(如果可用)。

各种 LEAF 函数都采用要加载的错误对象列表。例如,如果一个函数 copy_file(它将输入文件名和输出文件名作为其参数)检测到故障,则它可以传递错误代码 ec,以及使用 new_error 的两个相关文件名

return leaf::new_error(ec, e_input_name{n1}, e_output_name{n2});

或者,可以使用已经传递错误的 result<T> 加载错误对象。这样,它们就与该错误而不是新错误关联起来

leaf::result<int> f() noexcept;

leaf::result<void> g( char const * fn ) noexcept
{
  if( leaf::result<int> r = f() )
  { (1)
    ....;
    return { };
  }
  else
  {
    return r.load( e_file_name{fn} ); (2)
  }
}
1 成功!使用 r.value()
2 f() 失败;这里我们将额外的 e_file_name 与错误关联起来。但是,只有当通往 g 的调用堆栈中存在接受 e_file_name 参数的错误处理程序时,才会发生这种关联。否则,传递给 load 的对象将被丢弃。换句话说,只有当程序实际使用它们来处理错误时,才会加载传递的对象。

除了错误对象,load 还可以接受函数参数

  • 如果我们传递一个不带参数的函数,它将被调用,并且返回的错误对象将被加载。

    请考虑,如果我们传递给 load 一个错误处理程序未使用的错误对象,它将被丢弃。如果我们将返回错误对象的函数传递给 load 而不是错误对象,则只有在需要它返回的对象时,即如果不会被丢弃时,才会调用该函数。当错误对象的生成成本相对较高时,这很有用

    struct info { .... };
    
    info compute_info() noexcept;
    
    leaf::result<void> operation( char const * file_name ) noexcept
    {
      if( leaf::result<int> r = try_something() )
      { (1)
        ....
        return { };
      }
      else
      {
        return r.load( (2)
          [&]
          {
            return compute_info();
          } );
      }
    }
    1 成功!使用 r.value()
    2 try_something 失败;只有当调用堆栈中存在接受 info 参数的错误处理程序时,才会调用 compute_info
  • 如果我们传递一个函数,该函数接受类型为 E & 的单个参数,则 LEAF 使用当前加载在与错误关联的活动 context 中的类型为 E 的对象调用该函数。如果没有此类对象可用,则将默认初始化一个新对象,然后将其传递给函数。

    例如,如果涉及许多不同文件的操作失败,则程序可以提供用于在 e_relevant_file_names 对象中收集所有相关文件名的功能

    struct e_relevant_file_names
    {
      std::vector<std::string> value;
    };
    
    leaf::result<void> operation( char const * file_name ) noexcept
    {
      if( leaf::result<int> r = try_something() )
      { (1)
        ....
        return { };
      }
      else
      {
        return r.load( (2)
          [&](e_relevant_file_names & e)
          {
            e.value.push_back(file_name);
          } );
      }
    }
    1 成功!使用 r.value()
    2 try_something 失败 — 将 file_name 添加到与 r 中传递的 error_id 关联的 e_relevant_file_names 对象。但是,请注意,只有当调用堆栈中存在接受 e_relevant_file_names 对象的错误处理程序时,才会调用传递的函数。

使用 on_error

错误报告函数通常无法提供合适的错误处理函数为了从故障中恢复而需要的所有数据。例如,报告 FILE 故障的函数可能无法访问文件名,但错误处理函数需要该文件名才能打印有用的错误消息。

文件名通常在通往失败的 FILE 操作的调用堆栈中很容易获得。在下面,虽然 parse_info 无法报告文件名,但 parse_file 可以并且确实可以

leaf::result<info> parse_info( FILE * f ) noexcept; (1)

leaf::result<info> parse_file( char const * file_name ) noexcept
{
  auto load = leaf::on_error(leaf::e_file_name{file_name}); (2)

  if( FILE * f = fopen(file_name,"r") )
  {
    auto r = parse_info(f);
    fclose(f);
    return r;
  }
  else
    return leaf::new_error( error_enum::file_open_error );
}
1 parse_info 使用 leaf::result 传递错误。
2 on_error 确保文件名包含在 parse_file 报告的任何错误中。当 load 对象过期时,如果正在报告错误,则传递的 e_file_name 值将自动与之关联。
on_error — 像 new_error 一样 — 可以传递任意数量的参数。

当我们调用 on_error 时,我们可以传递三种参数

  1. 实际错误对象(如上面的示例);

  2. 不带参数并返回错误对象的函数;

  3. 通过可变引用获取单个错误对象的函数。

例如,如果我们想使用 on_error 捕获 errno,我们可以使用 e_errno 类型,它是一个简单的结构,包装了一个 int。但是,我们不能只将 e_errno 传递给 on_error,因为那时 errno 尚未设置(尚未)。相反,我们将传递一个返回它的函数

void read_file(FILE * f) {

  auto load = leaf::on_error([]{ return leaf::e_errno{errno}; });

  ....
  size_t nr1=fread(buf1,1,count1,f);
  if( ferror(f) )
    leaf::throw_exception();

  size_t nr2=fread(buf2,1,count2,f);
  if( ferror(f) )
    leaf::throw_exception();

  size_t nr3=fread(buf3,1,count3,f);
  if( ferror(f) )
    leaf::throw_exception();
  ....
}

在上面,如果抛出异常,LEAF 将调用传递给 on_error 的函数,并将返回的 e_errno 对象与异常关联起来。

最后,如果 on_error 传递了一个通过可变引用获取单个错误对象的函数,则行为类似于 load 处理此类函数的方式;请参阅 加载错误对象


使用谓词处理错误

通常,错误处理程序和可用错误对象之间的兼容性是根据它们获取的参数类型来确定的。当错误处理程序将谓词类型作为参数时,处理程序选择过程 也能够考虑可用错误对象的

考虑这个错误代码枚举

enum class my_error
{
  e1=1,
  e2,
  e3
};

我们可以像这样处理 my_error 错误

return leaf::try_handle_some(

  []
  {
    return f(); // Returns leaf::result<T>
  },

  []( my_error e ) // handle my_error objects
  {
    switch(e)
    {
      case my_error::e1:
        ....; // Handle e1 error values
        break;
      case my_error::e2:
      case my_error::e3:
        ....; // Handle e2 and e3 error values
        break;
      default:
        ....; // Handle bad my_error values
        break;
  } );

如果 my_error 对象可用,LEAF 将调用我们的错误处理程序。如果不可用,则故障将转发给调用方。

可以使用 match 谓词重写此代码,以在不同的错误处理程序中组织不同的情况。以下是等效的

return leaf::try_handle_some(

  []
  {
    return f(); // returns leaf::result<T>
  },

  []( leaf::match<my_error, my_error::e1> m )
  {
    assert(m.matched == my_error::e1);
    ....;
  },

  []( leaf::match<my_error, my_error::e2, my_error::e3> m )
  {
    assert(m.matched == my_error::e2 || m.matched == my_error::e3);
    ....;
  },

  []( my_error e )
  {
    ....;
  } );

match 模板的第一个参数通常指定错误对象 e 的类型 E,该类型必须可用才能考虑错误处理程序。通常,其余参数是值。如果 e 与它们中的任何一个都不相等,则会删除错误处理程序。

特别是,matchstd::error_code 非常有效。以下处理程序旨在处理 ENOENT 错误

[]( leaf::match<std::error_code, std::errc::no_such_file_or_directory> )
{
}

但是,这需要 C++17 或更高版本。LEAF 提供了以下与 C++11 兼容的解决方法

[]( leaf::match<leaf::condition<std::errc>, std::errc::no_such_file_or_directory> )
{
}

也可以根据 std::error_category 选择处理程序。以下处理程序将匹配 std::generic_category 的任何 std::error_code(需要 C++17 或更高版本)

[]( std::error_code, leaf::category<std::errc>> )
{
}
有关更多示例,请参阅 match

以下谓词可用

  • match:如上所述。

  • match_value:其中 match<E, V…​> 将类型为 E 的对象 e 与值 V…​ 进行比较,match_value<E, V…​>e.value 与值 V…​ 进行比较。

  • match_member:类似于 match_value,但采用指向要比较的数据成员的指针;也就是说,match_member<&E::value, V…​> 等效于 match_value<E, V…​>。但是,请注意,match_member 需要 C++17 或更高版本,而 match_value 则不需要。

  • catch_<Ex…​>:类似于 match,但检查捕获的 std::exception 对象是否可以 dynamic_cast 到任何 Ex 类型。

  • if_not 是一个特殊的谓词,它接受任何其他谓词 Pred,并要求类型为 E 的错误对象可用,并且 Pred 的计算结果为 false。例如,if_not<match<E, V…​>> 要求类型为 E 的对象 e 可用,并且它与任何指定的 V…​ 都不相等。

谓词系统易于扩展,请参阅 谓词


重用通用错误处理程序

考虑以下代码段

leaf::try_handle_all(

  [&]
  {
    return f(); // returns leaf::result<T>
  },

  [](my_error_enum x)
  {
    ...
  },

  [](read_file_error_enum y, e_file_name const & fn)
  {
    ...
  },

  []
  {
    ...
  });

如果我们需要尝试一组不同的操作,但仍使用相同的处理程序,我们可以重复相同的操作,并将不同的函数作为 try_handle_allTryBlock 传递

leaf::try_handle_all(

  [&]
  {
    return g(); // returns leaf::result<T>
  },

  [](my_error_enum x)
  {
    ...
  },

  [](read_file_error_enum y, e_file_name const & fn)
  {
    ...
  },

  []
  {
    ...
  });

这可以工作,但也可以将错误处理程序绑定在 std::tuple

auto error_handlers = std::make_tuple(

  [](my_error_enum x)
  {
    ...
  },

  [](read_file_error_enum y, e_file_name const & fn)
  {
    ...
  },

  []
  {
    ...
  });

error_handlers 元组稍后可以与任何错误处理函数一起使用

leaf::try_handle_all(

  [&]
  {
    // Operations which may fail (1)
  },

  error_handlers );

leaf::try_handle_all(

  [&]
  {
    // Different operations which may fail (2)
  },

  error_handlers ); (3)
1 一组可能失败的操作……
2 另一组可能失败的操作……
3 ……两者都使用相同的 error_handlers

错误处理函数接受 std::tuple 错误处理程序,以代替任何错误处理程序。其行为就好像元组是就地解包的一样。


在线程之间传输错误

像异常一样,LEAF 错误对象是线程本地的。当使用并发时,有时我们需要在一个线程中收集错误对象,然后使用它们在另一个线程中处理错误。

LEAF 支持此功能,无论是否使用异常处理。在这两种情况下,错误对象都在 leaf::result<T> 对象中捕获和传输。

在不使用异常处理的情况下在线程之间传输错误

假设我们有一个要异步启动的 task,它生成 task_result,但也可能失败

leaf::result<task_result> task();

由于任务将异步运行,因此在发生故障时,我们需要捕获任何生成的错误对象,但不处理错误。我们通过调用 try_capture_all 来做到这一点

std::future<leaf::result<task_result>> launch_task() noexcept
{
  return std::async(
    std::launch::async,
    [&]
    {
      return leaf::try_capture_all(task);
    } );
}

如果发生故障,从 try_capture_all 返回的 result<T> 对象将保存从 task 传出的所有错误对象,但代价是动态分配。然后可以将 result<T> 对象隐藏起来或移动到另一个线程,稍后传递给错误处理函数以卸载其内容并处理错误

//std::future<leaf::result<task_result>> fut;
fut.wait();

return leaf::try_handle_some(

  [&]() -> leaf::result<void>
  {
    BOOST_LEAF_AUTO(r, fut.get());
    //Success!
    return { }
  },

  [](E1 e1, E2 e2)
  {
    //Deal with E1, E2
    ....
    return { };
  },

  [](E3 e3)
  {
    //Deal with E3
    ....
    return { };
  } );
请点击此链接查看完整的示例程序:try_capture_all_result.cpp

在使用异常处理的情况下在线程之间传输错误

假设我们有一个异步 task,它生成 task_result,但也可能抛出异常

task_result task();

我们使用 try_capture_all 将所有错误对象和 std::current_exception() 捕获到 result<T>

std::future<leaf::result<task_result>> launch_task()
{
  return std::async(
    std::launch::async,
    [&]
    {
      return leaf::try_capture_all(task);
    } );
}

为了在等待 future 后处理错误,我们像往常一样使用 try_catch

//std::future<leaf::result<task_result>> fut;
fut.wait();

return leaf::try_catch(

  [&]
  {
    leaf::result<task_result> r = fut.get();
    task_result v = r.value(); // throws on error
    //Success!
  },

  [](E1 e1, E2 e2)
  {
    //Deal with E1, E2
    ....
  },

  [](E3 e3)
  {
    //Deal with E3
    ....
  } );
请点击此链接查看完整的示例程序:try_capture_all_exceptions.cpp

故障分类

接口通常定义一个 enum,其中列出了 API 报告的所有可能的错误代码。这种方法的好处是列表是完整的并且通常有很好的文档记录

enum error_code
{
  ....
  read_error,
  size_error,
  eof_error,
  ....
};

这种平面枚举的缺点是它们不支持处理整类故障。考虑以下 LEAF 错误处理程序

....
[](leaf::match<error_code, size_error, read_error, eof_error>, leaf::e_file_name const & fn)
{
  std::cerr << "Failed to access " << fn.value << std::endl;
},
....

如果与故障一起传递的 error_code 枚举的值是 size_errorread_erroreof_error 之一,则将调用它。简而言之,其思想是处理任何输入错误。

但是,如果稍后我们添加了对检测和报告新型输入错误(例如 permissions_error)的支持怎么办?很容易将其添加到我们的 error_code 枚举中;但是现在我们的输入错误处理程序将无法识别这个新的输入错误 — 并且我们有一个错误。

使用异常是一种改进,因为异常类型可以组织在层次结构中,以便对故障进行分类

struct input_error: std::exception { };
struct read_error: input_error { };
struct size_error: input_error { };
struct eof_error: input_error { };

就 LEAF 而言,我们的输入错误异常处理程序现在看起来像这样

[](input_error &, leaf::e_file_name const & fn)
{
  std::cerr << "Failed to access " << fn.value << std::endl;
},

这是面向未来的,但仍然不理想,因为在抛出异常对象后无法细化故障的分类。

LEAF 支持一种新颖的错误处理风格,其中故障的分类不使用错误代码值或异常类型层次结构。代替我们的 error_code 枚举,我们可以定义

....
struct input_error { };
struct read_error { };
struct size_error { };
struct eof_error { };
....

有了这个,我们可以定义一个函数 file_read

leaf::result<void> file_read( FILE & f, void * buf, int size )
{
  int n = fread(buf, 1, size, &f);

  if( ferror(&f) )
    return leaf::new_error(input_error{}, read_error{}, leaf::e_errno{errno}); (1)

  if( n!=size )
    return leaf::new_error(input_error{}, eof_error{}); (2)

  return { };
}
1 此错误分类为 input_errorread_error
2 此错误分类为 input_erroreof_error

或者,更好的是

leaf::result<void> file_read( FILE & f, void * buf, int size )
{
  auto load = leaf::on_error(input_error{}); (1)

  int n = fread(buf, 1, size, &f);

  if( ferror(&f) )
    return leaf::new_error(read_error{}, leaf::e_errno{errno}); (2)

  if( n!=size )
    return leaf::new_error(eof_error{}); (3)

  return { };
}
1 任何超出此范围的错误都将分类为 input_error
2 此外,此错误分类为 read_error
3 此外,此错误分类为 eof_error

如果我们选择使用异常处理,此技术同样有效,我们只需调用 leaf::throw_exception 而不是 leaf::new_error

void file_read( FILE & f, void * buf, int size )
{
  auto load = leaf::on_error(input_error{});

  int n = fread(buf, 1, size, &f);

  if( ferror(&f) )
    leaf::throw_exception(read_error{}, leaf::e_errno{errno});

  if( n!=size )
    leaf::throw_exception(eof_error{});
}
如果传递给 leaf::throw_exception 的第一个参数的类型派生自 std::exception,则将使用它来初始化抛出的异常对象。这里不是这种情况,因此该函数抛出一个默认初始化的 std::exception 对象,而第一个(和任何其他)参数与故障关联。

现在我们可以为任何 input_error 编写一个面向未来的处理程序

....
[](input_error, leaf::e_file_name const & fn)
{
  std::cerr << "Failed to access " << fn.value << std::endl;
},
....

值得注意的是,由于故障的分类不依赖于错误代码或异常类型,因此如果我们使用异常处理,则可以将此错误处理程序与 try_catch 一起使用;如果我们不使用异常处理,则可以与 try_handle_some/try_handle_all 一起使用。


将异常转换为 result<T>

有时需要捕获较低级别库函数抛出的异常,并通过不同的方式向可能不使用异常处理的更高级别库报告错误。

接受类型派生自 std::exception 的参数的错误处理程序可以正常工作 — 无论错误对象本身是否作为异常抛出,还是 加载context 中。此处描述的技术仅在必须通过非异常安全或在禁用异常处理的情况下编译的函数传递异常时才需要。

假设我们有一个异常类型层级结构和一个函数 compute_answer_throws

class error_base: public std::exception { };
class error_a: public error_base { };
class error_b: public error_base { };
class error_c: public error_base { };

int compute_answer_throws()
{
  switch( rand()%4 )
  {
    default: return 42;
    case 1: throw error_a();
    case 2: throw error_b();
    case 3: throw error_c();
  }
}

我们可以使用 exception_to_result 编写一个简单的包装器,它调用 compute_answer_throws 并切换到 result<int> 进行错误处理

leaf::result<int> compute_answer() noexcept
{
  return leaf::exception_to_result<error_a, error_b>(
    []
    {
      return compute_answer_throws();
    } );
}

exception_to_result 模板接受任意数量的异常类型。它会捕获被传递函数抛出的所有异常类型,并尝试将异常对象转换为每个指定的类型。每个成功转换的捕获异常对象的切片,以及 std::current_exception 的返回值,都会被复制和 加载,最终异常会被转换为一个 result<T> 对象。

(在我们的示例中,error_aerror_b 切片会作为错误对象进行传递,但 error_c 异常仍然会被 std::exception_ptr 捕获)。

这是一个简单的函数,它打印成功计算的答案,并将任何错误(最初通过抛出异常报告)转发给其调用者

leaf::result<void> print_answer() noexcept
{
  BOOST_LEAF_AUTO(answer, compute_answer());
  std::cout << "Answer: " << answer << std::endl;
  return { };
}

最后,这是处理错误的范围 — 无论 error_aerror_b 对象是否作为异常抛出,它都能正常工作。

leaf::try_handle_all(

  []() -> leaf::result<void>
  {
    BOOST_LEAF_CHECK(print_answer());
    return { };
  },

  [](error_a const & e)
  {
    std::cerr << "Error A!" << std::endl;
  },

  [](error_b const & e)
  {
    std::cerr << "Error B!" << std::endl;
  },

  []
  {
    std::cerr << "Unknown error!" << std::endl;
  } );
说明此技术的完整程序可在此处获取:here

使用 error_monitor 从 C 回调报告任意错误

传达与 C 回调中检测到的失败相关的信息是棘手的,因为 C 回调被限制为特定的函数签名,该签名可能不使用 C++ 类型。

LEAF 使这变得容易。作为一个例子,我们将编写一个程序,该程序使用 Lua 并从注册为 C 回调的 C++ 函数报告失败,该回调从 Lua 程序调用。失败将从 C++ 传播,通过 Lua 解释器(用 C 编写),再回到调用它的 C++ 函数。

旨在从 Lua 程序调用的 C/C++ 函数必须使用以下签名

int do_work( lua_State * L ) ;

参数在 Lua 堆栈(可以通过 L 访问)上传递。结果也推送到 Lua 堆栈上。

首先,让我们初始化 Lua 解释器并注册一个名为 do_work 的函数,作为可供 Lua 程序调用的 C 回调

std::shared_ptr<lua_State> init_lua_state() noexcept
{
  std::shared_ptr<lua_State> L(lua_open(), &lua_close); (1)

  lua_register(&*L, "do_work", &do_work); (2)

  luaL_dostring(&*L, "\ (3)
\n  function call_do_work()\
\n    return do_work()\
\n  end");

  return L;
}
1 创建一个新的 lua_State。我们将使用 std::shared_ptr 进行自动清理。
2 do_work C++ 函数注册为 C 回调,全局名称为 do_work。这样,从 Lua 程序到 do_work 的调用将落入 do_work C++ 函数中。
3 将一些 Lua 代码作为 C 字符串字面量传递给 Lua。这将创建一个名为 call_do_work 的全局 Lua 函数,我们稍后将要求 Lua 执行它。

接下来,让我们定义我们的 enum,用于传达 do_work 失败

enum do_work_error_code
{
  ec1=1,
  ec2
};

我们现在准备好定义 do_work 回调函数了

int do_work( lua_State * L ) noexcept
{
  bool success = rand() % 2; (1)
  if( success )
  {
    lua_pushnumber(L, 42); (2)
    return 1;
  }
  else
  {
    (void) leaf::new_error(ec1); (3)
    return luaL_error(L, "do_work_error"); (4)
  }
}
1 “有时” do_work 会失败。
2 如果成功,将结果推送到 Lua 堆栈上,返回到 Lua。
3 生成一个新的 error_id 并将其与 do_work_error_code 关联。通常,我们会在 leaf::result<T> 中返回它,但 do_work 函数签名(Lua 要求)不允许这样做。
4 告诉 Lua 解释器中止 Lua 程序。

现在我们将编写调用 Lua 解释器来执行 Lua 函数 call_do_work 的函数,该函数反过来调用 do_work。我们将返回 result<int>,以便我们的调用者可以在成功时获得答案,或者获得错误

leaf::result<int> call_lua( lua_State * L )
{
  lua_getfield(L, LUA_GLOBALSINDEX, "call_do_work");

  error_monitor cur_err;
  if( int err = lua_pcall(L, 0, 1, 0) ) (1)
  {
    auto load = leaf::on_error(e_lua_error_message{lua_tostring(L,1)}); (2)
    lua_pop(L,1);

    return cur_err.assigned_error_id().load(e_lua_pcall_error{err}); (3)
  }
  else
  {
    int answer = lua_tonumber(L, -1); (4)
    lua_pop(L, 1);
    return answer;
  }
}
1 要求 Lua 解释器调用全局 Lua 函数 call_do_work
2 on_error 像往常一样工作。
3 load 将使用在我们的 Lua 回调中生成的 error_id。这与 on_error 使用的 error_id 相同。
4 成功!只需返回 int 答案。

最后,这是 main 函数,它练习 call_lua,每次都处理任何失败

int main() noexcept
{
  std::shared_ptr<lua_State> L=init_lua_state();

  for( int i=0; i!=10; ++i )
  {
    leaf::try_handle_all(

      [&]() -> leaf::result<void>
      {
        BOOST_LEAF_AUTO(answer, call_lua(&*L));
        std::cout << "do_work succeeded, answer=" << answer << '\n'; (1)
        return { };
      },

      [](do_work_error_code e) (2)
      {
        std::cout << "Got do_work_error_code = " << e <<  "!\n";
      },

      [](e_lua_pcall_error const & err, e_lua_error_message const & msg) (3)
      {
        std::cout << "Got e_lua_pcall_error, Lua error code = " << err.value << ", " << msg.value << "\n";
      },

      [](leaf::error_info const & unmatched)
      {
        std::cerr << "Unknown failure detected\n" << unmatched;
      } );
  }
1 如果对 call_lua 的调用成功,则只需打印答案。
2 处理 do_work 失败。
3 处理所有其他 lua_pcall 失败。
点击此链接查看完整程序:lua_callback_result.cpp
将 Lua 与 C++ 一起使用时,我们需要保护 Lua 解释器免受可能从安装为 lua_CFunction 回调的 C++ 函数抛出的异常的影响。这是本节中的程序,经过重写以使用 C++ 异常(而不是 leaf::result)来安全地将错误从 do_work 函数传达出来:lua_callback_exceptions.cpp

诊断信息

LEAF 能够自动生成诊断消息,其中包括有关错误处理程序可用的所有错误对象的信息

enum class error_code
{
  read_error,
  write_error
};

....

leaf::try_handle_all(

  []() -> leaf::result<void> (1)
  {
    ...
    return leaf::new_error( error_code::write_error, leaf::e_file_name{ "file.txt" } );
  },

  []( leaf::match<error_code, error_code::read_error> ) (2)
  {
    std::cerr << "Read error!" << std::endl;
  },

  []( leaf::diagnostic_details const & info ) (3)
  {
    std::cerr << "Unrecognized error detected\n" << info;
  } );
1 我们处理在此 try 块中发生的所有失败。
2 一个或多个应该处理所有可能失败的错误处理程序。
3 try_handle_all 需要这个“捕获所有”错误处理程序。如果 LEAF 无法使用另一个错误处理程序,则将调用它。

上面代码片段的 diagnostic_details 输出告诉我们,我们得到了一个值为 1 (write_error) 的 error_code,以及一个类型为 e_file_name 的对象,其 .value 中存储了 "file.txt"

Unrecognized error detected
Error with serial #1
Caught:
  error_code: 1
Diagnostic details:
  boost::leaf::e_file_name: file.txt
diagnostic_details 输出中,Caught: 下的部分列出了错误处理程序作为参数接收的对象 — 这些是存储在堆栈上的对象。Diagnostic details: 下的部分列出了所有其他已传达的对象。如果我们没有提供一个接受 diagnostic_details 的处理程序,这些对象将被丢弃。

为了打印每个错误对象,LEAF 尝试绑定对 operator<< 的非限定调用,传递一个 std::ostream 和错误对象。如果失败,它还会尝试绑定接受错误类型 .valueoperator<<。如果这也无法编译,则错误对象值将不会出现在诊断消息中,尽管 LEAF 仍会打印其类型。

即使对于定义了可打印 .value 的错误类型,用户可能仍然希望为封闭的 struct 重载 operator<<,例如

struct e_errno
{
  int value;

  friend std::ostream & operator<<( std::ostream & os, e_errno const & e )
  {
    return os << e.value << ", \"" << strerror(e.value) << '"';
  }
};

上面的 e_errno 类型旨在保存 errno 值。定义的 operator<< 重载将在打印 e_errno 值时自动包含来自 strerror 的输出(LEAF 在 <boost/leaf/common.hpp> 中定义了 e_errno,以及其他常用的错误类型)。

使用 diagnostic_details 是有代价的。通常,当程序尝试传达当前调用堆栈中的任何错误处理范围中未使用的类型的错误对象时,它们会被丢弃,从而节省周期。但是,如果提供了接受 diagnostic_details 参数的错误处理程序,则此类对象将存储在堆上而不是被丢弃。

如果认为处理 diagnostic_details 的成本太高,请改用 diagnostic_info

leaf::try_handle_all(

  []() -> leaf::result<void>
  {
    ...
    return leaf::new_error( error_code::write_error, leaf::e_file_name{ "file.txt" } );
  },

  []( leaf::match<error_code, error_code::read_error> )
  {
    std::cerr << "Read error!" << std::endl;
  },

  []( leaf::diagnostic_info const & info )
  {
    std::cerr << "Unrecognized error detected\n" << info;
  } );

在这种情况下,输出可能如下所示

Unrecognized error detected
Error serial #1
Caught:
  error_code: 1

请注意,我们是如何缺少 Diagnostic details: 部分的。那是因为 e_file_name 对象已被 LEAF 丢弃,因为没有错误处理程序需要它。

自动生成的诊断消息对开发者友好,但对用户不友好。

使用 std::error_code, std::error_condition

简介

从阅读标准规范中不易理解 std::error_codestd::error_condition 之间的关系。本节解释了它们应该如何使用,以及 LEAF 如何与它们交互。

std::error_code 背后的思想是编码表示错误代码的整数值,以及该值的域。域由 std::error_category 引用表示。从概念上讲,std::error_code 就像一个 pair<std::error_category const &, int>

假设我们有这个 enum

enum class libfoo_error
{
  e1 = 1,
  e2,
  e3
};

我们希望能够在 std::error_code 对象中传输 libfoo_error 值。这会擦除它们的静态类型,使其能够跨 API 边界自由传递。为此,我们必须定义一个 std::error_category 来表示我们的 libfoo_error 类型

std::error_category const & libfoo_error_category()
{
  struct category: std::error_category
  {
    char const * name() const noexcept override
    {
      return "libfoo";
    }

    std::string message(int code) const override
    {
      switch( libfoo_error(code) )
      {
        case libfoo_error::e1: return "e1";
        case libfoo_error::e2: return "e2";
        case libfoo_error::e3: return "e3";
        default: return "error";
      }
    }
  };

  static category c;
  return c;
}

我们还需要通知标准库,libfoo_errorstd::error_code 兼容,并提供一个工厂函数,该函数可用于从 libfoo_error 值创建 std::error_code 对象

namespace std
{
  template <>
  struct is_error_code_enum<libfoo_error>: std::true_type
  {
  };
}

std::error_code make_error_code(libfoo_error e)
{
  return std::error_code(int(e), libfoo_error_category());
}

有了这些,如果我们收到一个 std::error_code,我们可以轻松检查它是否代表了我们感兴趣的某些 libfoo_error

std::error_code f();

....
auto ec = f();
if( ec == libfoo_error::e1 || ec == libfoo_error::e2 )
{
  // We got either a libfoo_error::e1 or a libfoo_error::e2
}

这之所以有效,是因为标准库检测到 std::is_error_code_enum<libfoo_error>::valuetrue,然后使用 make_error_code 创建一个 std::error_code 对象,它实际上用于与 ec 进行比较。

到目前为止还不错,但请记住,标准库还定义了另一种类型,即 std::error_condition。第一个令人困惑的事情是,就其物理表示而言,std::error_conditionstd::error_code 完全相同;也就是说,它也像一个 std::error_category 引用和一个 int 的对。为什么我们需要两种使用相同物理表示的不同类型?

回答这个问题的关键是理解 std::error_code 对象旨在从函数返回以指示失败。相反,std::error_condition 对象 绝不 应该被传达;它们的目的是解释正在传达的 std::error_code 值。其思想是,在一个给定的程序中,可能有多个不同的“物理”(可能是平台特定的)std::error_code 值,它们都指示相同的“逻辑”std::error_condition

这引出了关于 std::error_condition 的第二个令人困惑的事情:它使用相同的 std::error_category 类型,但用于完全不同的目的:指定哪些 std::error_code 值等同于哪些 std::error_condition 值。

假设除了 libfoo 之外,我们的程序还使用另一个库 libbar,它以 std::error_code 和不同的错误类别传达失败。也许 libbar_error 看起来像这样

enum class libbar_error
{
  e1 = 1,
  e2,
  e3,
  e4
};

// Boilerplate omitted:
// - libbar_error_category()
// - specialization of std::is_error_code_enum
// - make_error_code factory function for libbar_error.

我们现在可以使用 std::error_condition 来定义由 libfoolibbar 传达的 std::error_code 值表示的逻辑错误条件

enum class my_error_condition (1)
{
  c1 = 1,
  c2
};

std::error_category const & libfoo_error_category() (2)
{
  struct category: std::error_category
  {
    char const * name() const noexcept override
    {
      return "my_error_condition";
    }

    std::string message(int cond) const override
    {
      switch( my_error_condition(code) )
      {
        case my_error_condition::c1: return "c1";
        case my_error_condition::c2: return "c2";
        default: return "error";
      }
    }

    bool equivalent(std::error_code const & code, int cond) const noexcept
    {
      switch( my_error_condition(cond) )
      {
        case my_error_condition::c1: (3)
          return
            code == libfoo_error::e1 ||
            code == libbar_error::e3 ||
            code == libbar_error::e4;
        case my_error_condition::c2: (4)
          return
            code == libfoo_error::e2 ||
            code == libbar_error::e1 ||
            code == libbar_error::e2;
        default:
          return false;
      }
    }
  };

  static category c;
  return c;
}

namespace std
{
  template <> (5)
  class is_error_condition_enum<my_error_condition>: std::true_type
  {
  };
}

std::error_condition make_error_condition(my_error_condition e) (6)
{
  return std::error_condition(int(e), my_error_condition_error_category());
}
1 两个逻辑错误条件 c1c2 的枚举。
2 为表示 my_error_conditionstd::error_condition 对象定义 std::error_category
3 在这里,我们指定 libfoo:error::e1libbar_error::e3libbar_error::e4 中的任何一个在逻辑上都等同于 my_error_condition::c1,并且…
4 …​libfoo:error::e2libbar_error::e1libbar_error::e2 中的任何一个在逻辑上都等同于 my_error_condition::c2
5 此特化告诉标准库,my_error_condition 枚举旨在与 std::error_condition 一起使用。
6 my_error_condition 值创建 std::error_condition 对象的工厂函数。

呼!

现在,如果我们有一个 std::error_code 对象 ec,我们可以轻松检查它是否等同于 my_error_condition::c1,如下所示

if( ec == my_error_condition::c1 )
{
  // We have a c1 in our hands
}

再次记住,除了为用 my_error_condition 值初始化的 std::error_condition 对象定义 std::error_category 之外,我们不需要与实际的 std::error_condition 实例交互:它们在需要与 std::error_code 进行比较时创建,这几乎是它们的所有用途。

LEAF 中的支持

match 谓词可以用作 LEAF 错误处理程序的参数,以将 std::error_code 与给定的错误条件匹配。例如,要处理 my_error_condition::c1(见上文),我们可以使用

leaf::try_handle_some(

  []
  {
    return f(); // returns leaf::result<T>
  },

  []( leaf::match<std::error_code, my_error_condition::c1> m )
  {
    assert(m.matched == my_error_condition::c1);
    ....
  } );

有关更多示例,请参阅 match


Boost 异常集成

除了 Boost Exception 定义的 boost::get_error_info API 之外,还可以直接使用 LEAF 错误处理程序。考虑以下 boost::get_error_info 的用法

typedef boost::error_info<struct my_info_, int> my_info;

void f(); // Throws using boost::throw_exception

void g()
{
  try
  {
    f();
  },
  catch( boost::exception & e )
  {
    if( int const * x = boost::get_error_info<my_info>(e) )
      std::cerr << "Got my_info with value = " << *x;
  } );
}

我们可以重写 g 以使用 LEAF 访问 my_info

#include <boost/leaf/handle_errors.hpp>

void g()
{
  leaf::try_catch(

    []
    {
      f();
    },

    []( my_info x )
    {
      std::cerr << "Got my_info with value = " << x.value();
    } );
}

接受 my_info 意味着只有当捕获的异常对象携带 my_info 时,才会选择处理程序(LEAF 通过 boost::get_error_info 访问 my_info)。

也支持使用 match

void g()
{
  leaf::try_catch(

    []
    {
      f();
    },

    []( leaf::match_value<my_info, 42> )
    {
      std::cerr << "Got my_info with value = 42";
    } );
}

在上面,如果捕获的异常对象携带 my_info.value() 等于 42,则将选择处理程序。

示例

参见 github

概要

本节列出了 LEAF 中的每个公共头文件,并记录了它提供的定义。

LEAF 标头旨在最大限度地减少耦合

  • 报告或转发但不处理错误所需的标头比提供错误处理功能的标头更轻量。

  • 提供异常处理或抛出功能的标头与提供错误处理或报告但不使用异常的标头是分开的。

提供独立的单标头选项;请参阅 Distribution


错误报告

common.hpp

#include <boost/leaf/common.hpp>
namespace boost { namespace leaf {

  struct e_api_function { char const * value; };

  struct e_file_name { std::string value; };

  struct e_type_info_name { char const * value; };

  struct e_at_line { int value; };

  struct e_errno
  {
    int value;
    explicit e_errno(int value=errno);

    template <class CharT, class Traits>
    friend std::ostream & operator<<( std::basic_ostream<CharT, Traits> &, e_errno const &);
  };

  namespace windows
  {
    struct e_LastError
    {
      unsigned value;

      explicit e_LastError(unsigned value);

#if BOOST_LEAF_CFG_WIN32
      e_LastError();

      template <class CharT, class Traits>
      friend std::ostream & operator<<( std::basic_ostream<CharT, Traits> &, e_LastError const &);
#endif
    };
  }

} }

error.hpp

#include <boost/leaf/error.hpp>
namespace boost { namespace leaf {

  class error_id
  {
  public:

    error_id() noexcept;

    template <class Enum>
    error_id( Enum e, typename std::enable_if<std::is_error_code_enum<Enum>::value, Enum>::type * = 0 ) noexcept;

    error_id( std::error_code const & ec ) noexcept;

    int value() const noexcept;
    explicit operator bool() const noexcept;

    std::error_code to_error_code() const noexept;

    friend bool operator==( error_id a, error_id b ) noexcept;
    friend bool operator!=( error_id a, error_id b ) noexcept;
    friend bool operator<( error_id a, error_id b ) noexcept;

    template <class... Item>
    error_id load( Item && ... item ) const noexcept;

    template <class CharT, class Traits>
    friend std::ostream & operator<<( std::basic_ostream<CharT, Traits> &, error_id );
  };

  bool is_error_id( std::error_code const & ec ) noexcept;

  template <class... Item>
  error_id new_error( Item && ... item ) noexcept;

  error_id current_error() noexcept;

  ////////////////////////////////////////

  template <class Ctx>
  class context_activator
  {
    context_activator( context_activator const & ) = delete;
    context_activator & operator=( context_activator const & ) = delete;

  public:

    explicit context_activator( Ctx & ctx ) noexcept;
    context_activator( context_activator && ) noexcept;
    ~context_activator() noexcept;
  };

  template <class Ctx>
  context_activator<Ctx> activate_context( Ctx & ctx ) noexcept;

  template <class R>
  struct is_result_type: std::false_type
  {
  };

  template <class R>
  struct is_result_type<R const>: is_result_type<R>
  {
  };

} }

#define BOOST_LEAF_ASSIGN(v, r)\
  auto && <<temp>> = r;\
  if( !<<temp>> )\
    return <<temp>>.error();\
  v = std::forward<decltype(<<temp>>)>(<<temp>>).value()

#define BOOST_LEAF_AUTO(v, r)\
  BOOST_LEAF_ASSIGN(auto v, r)

#if BOOST_LEAF_CFG_GNUC_STMTEXPR

#define BOOST_LEAF_CHECK(r)\
  ({\
    auto && <<temp>> = (r);\
    if( !<<temp>> )\
        return <<temp>>.error();\
    std::move(<<temp>>);\
  }).value()

#else

#define BOOST_LEAF_CHECK(r)\
  {\
    auto && <<temp>> = r;\
    if( !<<temp>> )\
      return <<temp>>.error()\
  }

#endif

#define BOOST_LEAF_NEW_ERROR <<exact-definition-unspecified>>

exception.hpp

#include <boost/leaf/exception.hpp>
namespace boost { namespace leaf {

  template <class Ex, class... E> (1)
  [[noreturn]] void throw_exception( Ex &&, E && ... );

  template <class E1, class... E> (2)
  [[noreturn]] void throw_exception( E1 &&, E && ... );

  [[noreturn]] void throw_exception();

  template <class Ex, class... E> (1)
  [[noreturn]] void throw_exception( error_id id, Ex &&, E && ... );

  template <class E1, class... E> (2)
  [[noreturn]] void throw_exception( error_id id, E1 &&, E && ... );

  [[noreturn]] void throw_exception( error_id id );

  template <class... Ex, class F>
  <<result<T>-deduced>> exception_to_result( F && f ) noexcept;

} }

#define BOOST_LEAF_THROW_EXCEPTION <<exact-definition-unspecified>>
1 仅当 std::is_base_of<std::exception, Ex>::value 时启用。
2 仅当 !std::is_base_of<std::exception, E1>::value 时启用。

on_error.hpp

#include <boost/leaf/on_error.hpp>
namespace boost { namespace leaf {

  template <class... Item>
  <<unspecified-type>> on_error( Item && ... e ) noexcept;

  class error_monitor
  {
  public:

    error_monitor() noexcept;

    error_id check() const noexcept;
    error_id assigned_error_id() const noexcept;
  };

} }

参考:on_error | error_monitor

result.hpp

#include <boost/leaf/result.hpp>
namespace boost { namespace leaf {

  template <class T>
  class result
  {
  public:

    using value_type = T;

    // NOTE: Copy constructor implicitly deleted.
    result( result && r ) noexcept;

    template <class U, class = typename std::enable_if<std::is_convertible<U, T>::value>::type>
    result( result<U> && r ) noexcept;

    result() noexcept;

    result( T && v ) noexcept;

    result( T const & v );

    result( error_id err ) noexcept;

    template <class U, class = typename std::enable_if<std::is_convertible<U, T>::value>::type>
    result( U && u );

#if BOOST_LEAF_CFG_STD_SYSTEM_ERROR

    result( std::error_code const & ec ) noexcept;

    template <class Enum, class = typename std::enable_if<std::is_error_code_enum<Enum>::value, int>::type>
    result( Enum e ) noexcept;

#endif

    // NOTE: Assignment operator implicitly deleted.
    result & operator=( result && r ) noexcept;

    template <class U, class = typename std::enable_if<std::is_convertible<U, T>::value>::type>
    result & operator=( result<U> && r ) noexcept;

    bool has_value() const noexcept;
    bool has_error() const noexcept;
    explicit operator bool() const noexcept;

    T const & value() const &;
    T & value() &;
    T const && value() const &&;
    T && value() &&;

    T const * operator->() const noexcept;
    T * operator->() noexcept;

    T const & operator*() const & noexcept;
    T & operator*() & noexcept;
    T const && operator*() const && noexcept;
    T && operator*() && noexcept;

    <<unspecified-type>> error() noexcept;

    template <class... Item>
    error_id load( Item && ... item ) noexcept;

    void unload();

    template <class CharT, class Traits>
    friend std::ostream & operator<<( std::basic_ostream<CharT, Traits> &, result const & );
  };

  template <>
  class result<void>
  {
  public:

    using value_type = void;

    // NOTE: Copy constructor implicitly deleted.
    result( result && r ) noexcept;

    result() noexcept;

    result( error_id err ) noexcept;

#if BOOST_LEAF_CFG_STD_SYSTEM_ERROR

    result( std::error_code const & ec ) noexcept;

    template <class Enum, typename std::enable_if<std::is_error_code_enum<Enum>::value, Enum>::type>
    result( Enum e ) noexcept;

#endif

    // NOTE: Assignment operator implicitly deleted.
    result & operator=( result && r ) noexcept;

    explicit operator bool() const noexcept;

    void value() const;

    void const * operator->() const noexcept;
    void * operator->() noexcept;

    void operator*() const noexcept;

    <<unspecified-type>> error() noexcept;

    template <class... Item>
    error_id load( Item && ... item ) noexcept;

    void unload();

    template <class CharT, class Traits>
    friend std::ostream & operator<<( std::basic_ostream<CharT, Traits> &, result const &);
  };

  struct bad_result: std::exception { };

  template <class T>
  struct is_result_type<result<T>>: std::true_type
  {
  };

} }

参考:result | is_result_type


错误处理

context.hpp

#include <boost/leaf/context.hpp>
namespace boost { namespace leaf {

  template <class... E>
  class context
  {
    context( context const & ) = delete;
    context & operator=( context const & ) = delete;

  public:

    context() noexcept;
    context( context && x ) noexcept;
    ~context() noexcept;

    void activate() noexcept;
    void deactivate() noexcept;
    bool is_active() const noexcept;

    void unload( error_id ) noexcept;

    void print( std::ostream & os ) const;

    template <class CharT, class Traits>
    friend std::ostream & operator<<( std::basic_ostream<CharT, Traits> &, context const & );

    template <class R, class... H>
    R handle_error( R &, H && ... ) const;
  };

  template <class... H>
  using context_type_from_handlers = typename <<unspecified>>::type;

  template <class...  H>
  BOOST_LEAF_CONSTEXPR context_type_from_handlers<H...> make_context() noexcept;

  template <class...  H>
  BOOST_LEAF_CONSTEXPR context_type_from_handlers<H...> make_context( H && ... ) noexcept;

} }

diagnostics.hpp

#include <boost/leaf/diagnostics.hpp>
namespace boost { namespace leaf {

  class diagnostic_info: public error_info
  {
    //No public constructors

    template <class CharT, class Traits>
    friend std::ostream & operator<<( std::basic_ostream<CharT, Traits> &, diagnostic_info const & );
  };

  class diagnostic_details: public error_info
  {
    //No public constructors

    template <class CharT, class Traits>
    friend std::ostream & operator<<( std::basic_ostream<CharT, Traits> &, diagnostic_info const & );
  };

} }

handle_errors.hpp

#include <boost/leaf/handle_errors.hpp>
namespace boost { namespace leaf {

  template <class TryBlock, class... H>
  typename std::decay<decltype(std::declval<TryBlock>()().value())>::type
  try_handle_all( TryBlock && try_block, H && ... h );

  template <class TryBlock, class... H>
  typename std::decay<decltype(std::declval<TryBlock>()())>::type
  try_handle_some( TryBlock && try_block, H && ... h );

  template <class TryBlock, class... H>
  typename std::decay<decltype(std::declval<TryBlock>()())>::type
  try_catch( TryBlock && try_block, H && ... h );

#if BOOST_LEAF_CFG_CAPTURE
  template <class TryBlock>
  result<T> // T deduced depending on TryBlock return type
  try_capture_all( TryBlock && try_block );
#endif

  class error_info
  {
    //No public constructors

  public:

    error_id error() const noexcept;

    bool exception_caught() const noexcept;
    std::exception const * exception() const noexcept;

    template <class CharT, class Traits>
    friend std::ostream & operator<<( std::basic_ostream<CharT, Traits> &, error_info const & );
  };

} }

pred.hpp

#include <boost/leaf/pred.hpp>
namespace boost { namespace leaf {

  template <class T>
  struct is_predicate: std::false_type
  {
  };

  template <class E, auto... V>
  struct match
  {
    E matched;

    // Other members not specified
  };

  template <class E, auto... V>
  struct is_predicate<match<E, V...>>: std::true_type
  {
  };

  template <class E, auto... V>
  struct match_value
  {
    E matched;

    // Other members not specified
  };

  template <class E, auto... V>
  struct is_predicate<match_value<E, V...>>: std::true_type
  {
  };

  template <auto, auto...>
  struct match_member;

  template <class E, class T, T E::* P, auto... V>
  struct member<P, V...>
  {
    E matched;

    // Other members not specified
  };

  template <auto P, auto... V>
  struct is_predicate<match_member<P, V...>>: std::true_type
  {
  };

  template <class... Ex>
  struct catch_
  {
    std::exception const & matched;

    // Other members not specified
  };

  template <class Ex>
  struct catch_<Ex>
  {
    Ex const & matched;

    // Other members not specified
  };

  template <class... Ex>
  struct is_predicate<catch_<Ex...>>: std::true_type
  {
  };

  template <class Pred>
  struct if_not
  {
    E matched;

    // Other members not specified
  };

  template <class Pred>
  struct is_predicate<if_not<Pred>>: std::true_type
  {
  };

  template <class ErrorCodeEnum>
  bool category( std::error_code const & ec ) noexcept;

  template <class Enum, class EnumType = Enum>
  struct condition;

} }

to_variant.hpp

#include <boost/leaf/to_variant.hpp>
namespace boost { namespace leaf {

  // Requires at least C++17
  template <class... E, class TryBlock>
  std::variant<
    typename std::decay<decltype(std::declval<TryBlock>()().value())>::type
    std::tuple<
      std::optional<E>...>>
  to_variant( TryBlock && try_block );

} }

参考:to_variant

参考:函数

每个“参考”部分的内容均按字母顺序组织。

activate_context

#include <boost/leaf/error.hpp>
namespace boost { namespace leaf {

  template <class Ctx>
  context_activator<Ctx> activate_context( Ctx & ctx ) noexcept
  {
    return context_activator<Ctx>(ctx);
  }

} }
示例
leaf::context<E1, E2, E3> ctx;

{
  auto active_context = ctx.raii_activate(); (1)
} (2)
1 激活 ctx
2 自动停用 ctx

context_type_from_handlers

#include <boost/leaf/context.hpp>
namespace boost { namespace leaf {

  template <class... H>
  using context_type_from_handlers = typename <<unspecified>>::type;

} }
示例
auto error_handlers = std::make_tuple(

  [](e_this const & a, e_that const & b)
  {
    ....
  },

  [](leaf::diagnostic_info const & info)
  {
    ....
  },
  .... );

leaf::context_type_from_handlers<decltype(error_handlers)> ctx; (1)
1 ctx 的类型将为 context<e_this, e_that>,从指定的错误处理程序自动推导出来。
或者,可以通过调用 make_context 创建合适的上下文,或者通过调用 [make_shared_context] 动态分配上下文。

current_error

#include <boost/leaf/error.hpp>
namespace boost { namespace leaf {

  error_id current_error() noexcept;

} }
返回值

上次从调用线程调用 new_error 时返回的 error_id 值。

另请参阅 on_error

exception_to_result

#include <boost/leaf/exception.hpp>
namespace boost { namespace leaf {

  template <class... Ex, class F>
  <<result<T>-deduced>> exception_to_result( F && f ) noexcept;

} }

此函数可用于捕获来自较低级别库的异常,并将其转换为 result<T>

返回值

如果 f 返回类型 T,则 exception_to_result 返回 leaf::result<T>

效果
  1. 捕获所有异常,然后在 std::exception_ptr 对象中捕获 std::current_exception,该对象与返回的 result<T> 一起 加载

  2. 尝试使用 dynamic_cast 将捕获的异常转换为 Ex…​ 中的每种类型 Exi。如果到 Exi 的转换成功,则捕获的异常的 Exi 切片将与返回的 result<T> 一起加载。

接受异常类型(即派生自 std::exception 的类型)参数的错误处理程序,无论对象是作为异常抛出还是通过 new_error 传达(或使用 exception_to_result 转换),都将正常工作。
示例
int compute_answer_throws();

//Call compute_answer, convert exceptions to result<int>
leaf::result<int> compute_answer()
{
  return leaf::exception_to_result<ex_type1, ex_type2>(compute_answer_throws());
}

稍后我们可以像往常一样调用 try_handle_some / try_handle_all,传递接受 ex_type1ex_type2 的处理程序,例如通过引用

return leaf::try_handle_some(

  [] -> leaf::result<void>
  {
    BOOST_LEAF_AUTO(answer, compute_answer());
    //Use answer
    ....
    return { };
  },

  [](ex_type1 & ex1)
  {
    //Handle ex_type1
    ....
    return { };
  },

  [](ex_type2 & ex2)
  {
    //Handle ex_type2
    ....
    return { };
  },

  [](std::exception_ptr const & p)
  {
    //Handle any other exception from compute_answer.
    ....
    return { };
  } );
当处理程序接受异常类型(即派生自 std::exception 的类型)的参数时,如果对象被抛出,则参数将动态匹配(使用 dynamic_cast);否则(例如,在被 exception_to_result 转换后),它将仅基于其静态类型进行匹配(这与不派生自 std::exception 的类型使用的行为相同)。
另请参阅教程中的 将异常转换为 result<T>

make_context

#include <boost/leaf/context.hpp>
namespace boost { namespace leaf {

  template <class...  H>
  context_type_from_handlers<H...> make_context() noexcept
  {
    return { };
  }

  template <class...  H>
  context_type_from_handlers<H...> make_context( H && ... ) noexcept
  {
    return { };
  }

} }
示例
auto ctx = leaf::make_context( (1)
  []( e_this ) { .... },
  []( e_that ) { .... } );
1 decltype(ctx)leaf::context<e_this, e_that>

new_error

#include <boost/leaf/error.hpp>
namespace boost { namespace leaf {

  template <class... Item>
  error_id new_error(Item && ... item) noexcept;

} }
要求

每个 Item…​ 类型都必须是 no-throw movable 的。

效果

如同

error_id id = <<generate-new-unique-id>>;
return id.load(std::forward<Item>(item)...);
返回值

一个新的 error_id 值,该值在整个程序中是唯一的。

确保

id.value()!=0,其中 id 是返回的 error_id

new_error 丢弃在任何活动错误处理调用范围中未使用的错误对象。
当加载到 context 中时,类型为 E 的错误对象将覆盖先前加载的类型为 E 的对象(如果有)。

on_error

#include <boost/leaf/on_error.hpp>
namespace boost { namespace leaf {

  template <class... Item>
  <<unspecified-type>> on_error(Item && ... item) noexcept;

} }
要求

每个 Item…​ 类型都必须是 no-throw movable 的。

效果

所有 item…​ 对象都会被转发和存储,以及从 std::unhandled_exceptions 返回的值,存储到未指定类型的返回对象中,该对象应由 auto 捕获并在调用范围内保持活动状态。当该对象被销毁时,如果自调用 on_error 以来发生了错误,LEAF 将处理存储的项目以获取要与失败关联的错误对象。

发生错误时,LEAF 首先需要推导出一个 error_iderr,以便将错误对象与之关联。这是使用以下逻辑完成的

  • 如果自创建 on_error 返回的对象以来(由调用线程)调用了 new_error,则 err 使用 current_error 返回的值初始化;

  • 否则,如果 std::unhandled_exceptions 返回的值大于初始化期间返回的值,则 err 使用 new_error 返回的值初始化;

  • 否则,存储的 item…​ 对象将被丢弃,并且不会采取进一步操作(未发生错误)。

接下来,LEAF 的处理方式类似于

err.load(std::forward<Item>(item)...);

不同之处在于,与 load 不同,on_error 不会覆盖已与 err 关联的任何错误对象。

请参阅教程中的 使用 on_error

throw_exception

#include <boost/leaf/exception.hpp>
namespace boost { namespace leaf {

  template <class Ex, class... E> (1)
  [[noreturn]] void throw_exception( Ex && ex, E && ... e );

  template <class E1, class... E> (2)
  [[noreturn]] void throw_exception( E1 && e1, E && ... e );

  [[noreturn]] void throw_exception(); (3)

  template <class Ex, class... E> (4)
  [[noreturn]] void throw_exception( error_id id, Ex && ex, E && ... e );

  template <class E1, class... E> (5)
  [[noreturn]] void throw_exception( error_id id, E1 && e1, E && ... e );

  [[noreturn]] void throw_exception( error_id id ); (6)

} }

throw_exception 函数被重载:可以不带参数调用它,或者还有几种替代方法,使用基于传递参数类型的 std::enable_if 选择。所有重载都抛出一个异常

1 如果第一个参数不是 error_id 类型并且是异常对象,即当且仅当 Ex 公开派生自 std::exception 时选择。在这种情况下,抛出的异常是未指定类型,它公开派生自 Ex error_id,使得
  • 它的 Ex 子对象由 std::forward<Ex>(ex) 初始化;

  • 它的 error_id 子对象由 new_error(std::forward<E>(e)…​) 初始化。

2 如果第一个参数不是 error_id 类型且不是异常对象,则选择。在这种情况下,抛出的异常是未指定类型,它公开派生自 std::exception error_id,使得
  • 它的 std::exception 子对象是默认初始化的;

  • 它的 error_id 子对象由 new_error(std::forward<E1>(e1), std::forward<E>(e)…​) 初始化。

3 如果调用函数时不带参数,则抛出的异常是未指定类型,它公开派生自 std::exception error_id,使得
  • 它的 std::exception 子对象是默认初始化的;

  • 它的 error_id 子对象由 new_error() 初始化。

4 如果第一个参数是 error_id 类型,第二个参数是异常对象,即当且仅当 Ex 公开派生自 std::exception 时选择。在这种情况下,抛出的异常是未指定类型,它公开派生自 Ex error_id,使得
  • 它的 Ex 子对象由 std::forward<Ex>(ex) 初始化;

  • 它的 error_id 子对象由 id.load(std::forward<E>(e)…​) 初始化。

5 如果第一个参数是 error_id 类型,第二个参数不是异常对象,则选择。在这种情况下,抛出的异常是未指定类型,它公开派生自 std::exception error_id,使得
  • 它的 std::exception 子对象是默认初始化的;

  • 它的 error_id 子对象由 id.load(std::forward<E1>(e1), std::forward<E>(e)…​) 初始化。

6 如果仅使用 error_id 对象调用 exception,则抛出的异常是未指定类型,它公开派生自 std::exception error_id,使得
  • 它的 std::exception 子对象是默认初始化的;

  • 它的 error_id 子对象通过从 id 复制来初始化。

前三个重载抛出一个与新的 error_id 关联的异常对象。后三个重载抛出一个与指定的 error_id 关联的异常对象。
示例 1
struct my_exception: std::exception { };

leaf::throw_exception(my_exception{}); (1)
1 抛出一个类型派生自 error_idmy_exception 的异常(因为 my_exception 派生自 std::exception)。
示例 2
enum class my_error { e1=1, e2, e3 }; (1)

leaf::throw_exception(my_error::e1);
1 抛出一个类型派生自 error_idstd::exception 的异常(因为 my_error 不派生自 std::exception)。
要自动捕获 __FILE____LINE____FUNCTION__ 与返回的对象,请使用 BOOST_LEAF_THROW_EXCEPTION 而不是 leaf::throw_exception

to_variant

#include <boost/leaf/to_variant.hpp>
namespace boost { namespace leaf {

  template <class... E, class TryBlock>
  std::variant<
    typename std::decay<decltype(std::declval<TryBlock>()().value())>::type
    std::tuple<
      std::optional<E>...>>
  to_variant( TryBlock && try_block );

} }
要求
  • 此功能仅在 C++-17 或更新版本下可用。

  • try_block 函数可能不带任何参数。

  • try_block 函数返回的类型必须是 result<T> 类型(参见 is_result_type)。try_block 返回 leaf::result<T> 是有效的,但这并非必需。

to_variant 函数在内部使用 try_handle_all 来调用 try_block 并将结果捕获到 std::variant 中。成功时,variant 包含来自生成的 result<T>T 对象。否则,variant 包含一个 std::tuple,其中每个 std::optional 元素包含用户提供的序列 E…​ 中类型为 Ei 的对象,或者如果失败未生成该类型的错误对象,则为空。

示例
enum class E1 { e11, e12, e13 };
enum class E2 { e21, e22, e23 };
enum class E3 { e31, e32, e33 };

....

auto v = leaf::to_variant<E1, E2, E3>(
  []() -> leaf::result<int>
  {
    return leaf::new_error( E1::e12, E3::e33 );
  } );

assert(v.index() == 1); (1)
auto t = std::get<1>(v); (2)

assert(std::get<0>(t).value() == E1::e12); (3)
assert(!std::get<1>(t).has_value()); (4)
assert(std::get<2>(t).value() == E3::e33); (3)
1 我们报告一个失败,因此 variant 必须包含错误对象元组,而不是 int
2 获取错误元组。
3 我们传达了一个 E1 和一个 E3 错误对象…
4 …​但没有 E2 错误对象。

try_capture_all

#include <boost/leaf/handle_errors.hpp>
#if BOOST_LEAF_CFG_CAPTURE

namespace boost { namespace leaf {

  template <class TryBlock>
  result<T> // T deduced depending on TryBlock return type
  try_capture_all( TryBlock && try_block ) noexcept;

} }

#endif
返回类型

leaf::result<T> 的实例,其中 T 根据 TryBlock 的返回类型 R 推导出来

  • 如果 R 是某种类型 Result<T>,对于该类型 is_result_type 为真,则 try_capture_all 返回 leaf::result<T>

  • 否则,假定 TryBlock 通过抛出异常来报告错误,并且 try_capture_all 的返回值被推导为 leaf::result<R>

效果

try_capture_all 执行 try_block,捕获并捕获返回的 leaf::result 对象中的所有异常和所有传达的错误对象。错误对象是动态分配的。

try_capture_all 的调用不得嵌套在 try_handle_all/try_handle_some/try_catch 或另一个 try_capture_all 中。
BOOST_LEAF_CFG_CAPTURE=0 下,try_capture_all 不可用。

try_catch

#include <boost/leaf/handle_errors.hpp>
namespace boost { namespace leaf {

  template <class TryBlock, class... H>
  typename std::decay<decltype(std::declval<TryBlock>()())>::type
  try_catch( TryBlock && try_block, H && ... h );

} }

try_catch 函数的工作方式类似于 try_handle_some,不同之处在于它不使用或理解 result<T> 类型的语义;相反

  • 它假定 try_block 抛出异常以指示失败,在这种情况下,try_catch 将尝试在 h…​ 中找到合适的处理程序;

  • 如果找不到合适的处理程序,则使用 throw; 重新抛出原始异常。

请参阅 异常处理

try_handle_all

#include <boost/leaf/handle_errors.hpp>
namespace boost { namespace leaf {

  template <class TryBlock, class... H>
  typename std::decay<decltype(std::declval<TryBlock>()().value())>::type
  try_handle_all( TryBlock && try_block, H && ... h );

} }

try_handle_all 函数的工作方式类似于 try_handle_some,除了

  • 此外,它要求 h…​ 中至少有一个可以用于处理任何错误(此要求在编译时强制执行);

  • 如果 try_block 返回某种 result<T> 类型,则必须可以使用 h…​ 中的每一个返回的值来初始化类型为 T 的值,并且

  • 由于需要处理所有错误,try_handle_all 解包由 try_block 返回的 result<T> 对象 r,返回 r.value() 而不是 r

请参阅 错误处理

try_handle_some

#include <boost/leaf/handle_errors.hpp>
namespace boost { namespace leaf {

  template <class TryBlock, class... H>
  typename std::decay<decltype(std::declval<TryBlock>()())>::type
  try_handle_some( TryBlock && try_block, H && ... h );

} }
要求
  • try_block 函数可能不带任何参数。

  • try_block 函数返回的类型必须是 result<T> 类型(参见 is_result_type)。try_block 返回 leaf::result<T> 是有效的,但这并非必需。

  • 每个 h…​ 函数

    • 必须返回可用于初始化类型 R 的对象的类型;如果 R 是 result<void>(即,在成功的情况下,它不传达值),则允许返回 void 的处理程序。如果选择了这样的处理程序,则 try_handle_some 返回值由 {} 初始化;

    • 可以按值、按(const)引用或作为指针(指向 const)获取任何错误对象;

    • 可以按值获取任何谓词类型的参数:catch_matchmatch_valuematch_memberif_not,或任何用户定义的谓词类型 Pred,对于该类型 is_predicate<Pred>::valuetrue

    • 可以按 const & 获取 error_info 参数;

    • 可以按 const & 获取 diagnostic_info 参数;

    • 可以按 const & 获取 diagnostic_details 参数。

效果
  • 创建一个本地 context<E…​> 对象 ctx,其中 E…​ 类型是从每个 h…​ 接受的参数类型自动推导出来的,这保证了 ctx 能够存储处理错误所需的所有类型。

  • 调用 try_block

    • 如果返回的对象 r 指示成功 并且 try_block 没有抛出异常,则 r 将转发给调用方。

    • 否则,LEAF 会按顺序考虑每个 h…​ 处理程序,直到找到一个可以使用当前存储在 ctx 中,与 r.error() 关联的错误对象为其提供参数的处理程序。第一个这样的处理程序被调用,并且其返回值用于初始化 try_handle_some 的返回值,如果处理程序能够处理错误,则返回值可以指示成功,如果不能,则指示失败。

    • 如果 try_handle_some 无法找到合适的处理程序,则返回 r

try_handle_some 是异常中立的:它不会抛出异常,但是允许 try_block 和任何 h…​ 抛出异常。
处理程序选择过程

一个处理程序 h 适合处理 r 报告的失败,当且仅当 try_handle_some 能够生成值作为其参数传递,使用当前在 ctx 中可用的错误对象,这些错误对象与通过调用 r.error() 获取的错误 ID 相关联。一旦确定无法生成参数值,则会丢弃当前处理程序,并继续选择下一个处理程序(如果有)。

r.error() 的返回值必须可以隐式转换为 error_id。当然,leaf::result 模板满足此要求。如果使用外部 result 类型,通常 r.error() 会返回一个 std::error_code,它可以传递 LEAF 错误 ID;请参阅 互操作性

如果 err 是从 r.error() 获取的 error_id,则当前正在考虑的处理程序所接受的每个参数 ai 的生成方式如下

  • 如果 ai 的类型为 AiAi const&Ai&

    • 如果类型为 Ai 的错误对象(与 err 关联)当前在 ctx 中可用,则 ai 将初始化为对该对象的引用;否则

    • 如果 Ai 派生自 std::exception,并且 try_block 抛出一个派生自 std::exception 的类型的对象 ex,则 LEAF 获取 Ai* p = dynamic_cast<Ai*>(&ex)。如果 p 为空,则丢弃处理程序,否则 ai 初始化为 *p

    • 否则,丢弃处理程序。

    示例
    ....
    auto r = leaf::try_handle_some(
    
      []() -> leaf::result<int>
      {
        return f();
      },
    
      [](leaf::e_file_name const & fn) (1)
      {
        std::cerr << "File Name: \"" << fn.value << '"' << std::endl; (2)
    
        return 1;
      } );
    1 如果 try_block 指示失败,则当 ctx 存储与错误关联的 e_file_name 时,将选择此处理程序。由于这是唯一提供的处理程序,如果 e_file_name 不可用,try_handle_some 将返回由 f 返回的 leaf::result<int>
    2 打印文件名,处理错误。
  • 如果 ai 的类型为 Ai const*Ai*,则 try_handle_some 始终能够生成它:首先它尝试像按引用获取那样生成它;如果失败,则不是丢弃处理程序,而是将 ai 初始化为 0

    示例
    ....
    try_handle_some(
    
      []() -> leaf::result<int>
      {
        return f();
      },
    
      [](leaf::e_file_name const * fn) (1)
      {
        if( fn ) (2)
          std::cerr << "File Name: \"" << fn->value << '"' << std::endl;
    
        return 1;
      } );
    }
    1 此处理程序可以被选择来处理任何错误,因为它将 e_file_name 作为 const *(并且没有其他)。
    2 如果当前错误有 e_file_name 可用,则打印它。
  • 如果 ai 的类型是谓词类型 Pred(对于该类型,is_predicate<Pred>::valuetrue),则 E 推导为 typename Pred::error_type,然后

    • 如果 E 不是 void,并且类型为 E 的错误对象 e(与 err 关联)当前未存储在 ctx 中,则丢弃处理程序;否则,如果表达式 Pred::evaluate(e) 返回 false,则丢弃处理程序。

    • 如果 Evoid,并且没有捕获 std::exception,则丢弃处理程序;否则,如果表达式 Pred::evaluate(e)(其中 e 的类型为 std::exception const &)返回 false,则丢弃处理程序。

    • 要调用处理程序,Pred 参数 ai 使用 Pred{e} 初始化。

      另请参阅:谓词
  • 如果 ai 的类型为 error_info const &,则 try_handle_some 始终能够生成它。

    示例
    ....
    try_handle_some(
    
      []
      {
        return f(); // returns leaf::result<T>
      },
    
      [](leaf::error_info const & info) (1)
      {
        std::cerr << "leaf::error_info:\n" << info; (2)
        return info.error(); (3)
      } );
    1 此处理程序匹配任何错误。
    2 打印错误信息。
    3 返回原始错误,它将从 try_handle_some 中返回。
  • 如果 ai 的类型为 diagnostic_info const &,则 try_handle_some 始终能够生成它。

    示例
    ....
    try_handle_some(
    
      []
      {
        return f(); // throws
      },
    
      [](leaf::diagnostic_info const & info) (1)
      {
        std::cerr << "leaf::diagnostic_information:\n" << info; (2)
        return info.error(); (3)
      } );
    1 此处理程序匹配任何错误。
    2 打印诊断信息,包括有关已丢弃错误对象的有限信息。
    3 返回原始错误,它将从 try_handle_some 中返回。
  • 如果 ai 的类型为 diagnostic_details const &,则 try_handle_some 始终能够生成它。

    示例
    ....
    try_handle_some(
    
      []
      {
        return f(); // throws
      },
    
      [](leaf::diagnostic_details const & info) (1)
      {
        std::cerr << "leaf::diagnostic_details\n" << info; (2)
        return info.error(); (3)
      } );
    1 此处理程序匹配任何错误。
    2 打印详细的诊断信息,包括已丢弃错误对象的值。
    3 返回原始错误,它将从 try_handle_some 中返回。

参考:类型

每个“参考”部分的内容均按字母顺序组织。

context

#include <boost/leaf/context.hpp>
namespace boost { namespace leaf {

  template <class... E>
  class context
  {
    context( context const & ) = delete;
    context & operator=( context const & ) = delete;

  public:

    context() noexcept;
    context( context && x ) noexcept;
    ~context() noexcept;

    void activate() noexcept;
    void deactivate() noexcept;
    bool is_active() const noexcept;

    void unload( error_id ) noexcept;

    void print( std::ostream & os ) const;

    template <class R, class... H>
    R handle_error( error_id, H && ... ) const;

  };

  template <class... H>
  using context_type_from_handlers = typename <<unspecified>>::type;

} }

context 类模板为每个指定的 E…​ 类型提供存储。通常,context 对象不直接使用;当调用 try_handle_sometry_handle_alltry_catch 函数时,会内部创建它们,并使用从传递的处理程序的参数类型自动推导出的类型实例化。

独立地,如果用户需要捕获错误对象然后通过移动 context 对象本身来传输它们,则可以创建 context 对象。

即使在这种情况下,也建议用户不要通过显式列出他们希望 context 能够存储的 E…​ 类型来实例化 context 模板。而是使用 context_type_from_handlers 或调用 make_context 函数模板,它们从捕获的处理程序函数对象列表中推导出正确的 E…​ 类型。

为了能够在 context 对象中加载错误对象,必须激活它。激活 context 对象 ctx 会将其绑定到调用线程,并将存储的 E…​ 类型的线程局部指针设置为指向 ctx 中的相应存储。在一个给定的线程中,有可能甚至很可能拥有多个活动的 context。在这种情况下,激活/停用必须以 LIFO 方式发生。因此,最好使用 context_activator,它依赖 RAII 来激活和停用 context

context 被停用时,它会从调用线程分离,将线程局部指针恢复到其 pre-activate 值。通常,此时,存储的错误对象(如果有)要么被丢弃(默认情况下),要么通过调用 unload 移动到调用线程中其他活动的 context 对象中的相应存储中(如果可用)。

虽然错误处理通常使用 try_handle_sometry_handle_alltry_catch,但也可以通过调用成员函数 handle_error 来处理错误。它接受一个 error_id,并尝试根据存储在 *this 中的与传递的 error_id 关联的错误对象来选择错误处理程序。

context 对象可以移动,只要它们不是活动的。
移动活动的 context 会导致未定义的行为。

构造函数

#include <boost/leaf/context.hpp>
namespace boost { namespace leaf {

  template <class... E>
  context<E...>::context() noexcept;

  template <class... E>
  context<E...>::context( context && x ) noexcept;

} }

默认构造函数初始化一个空的 context 对象:它提供存储,但不包含任何错误对象。

移动构造函数将存储的错误对象从一个 context 移动到另一个 context

移动活动的 context 对象会导致未定义的行为。

activate

#include <boost/leaf/context.hpp>
namespace boost { namespace leaf {

  template <class... E>
  void context<E...>::activate() noexcept;

} }
要求

!is_active().

效果

*this 与调用线程关联。

确保

is_active().

当上下文与线程关联时,线程局部指针设置为指向其存储中的每个 E…​ 类型,同时每个此类指针的先前值都保存在 context 对象中,以便可以通过调用 deactivate 来撤消 activate 的效果。

当错误对象被 加载 时,它会被移动到调用线程中最后激活的(在调用线程中)context 对象中,该对象为其类型提供存储(请注意,这可能不是最后激活的 context 对象)。如果没有这样的存储可用,则错误对象将被丢弃。


deactivate

#include <boost/leaf/context.hpp>
namespace boost { namespace leaf {

  template <class... E>
  void context<E...>::deactivate() noexcept;

} }
要求
  • is_active();

  • *this 必须是调用线程中最后激活的 context 对象。

效果

取消 *this 与调用线程的关联。

确保

!is_active().

当上下文被停用时,当前指向其中每个单独错误对象存储的线程局部指针将恢复为其在调用 activate 之前的原始值。


handle_error

#include <boost/leaf/handle_errors.hpp>
namespace boost { namespace leaf {

  template <class... E>
  template <class R, class... H>
  R context<E...>::handle_error( error_id err, H && ... h ) const;

} }

此函数的工作方式类似于 try_handle_all,但它不是调用 try_block 并从返回的 result 类型中获取 error_id,而是将错误对象(存储在 *this 中,与 err 关联)与 h…​ 包中的合适错误处理程序匹配。

调用者需要指定返回类型 R。这是因为通常提供的处理程序可能返回不同的类型(所有这些类型都必须可转换为 R)。

is_active

#include <boost/leaf/context.hpp>
namespace boost { namespace leaf {

  template <class... E>
  bool context<E...>::is_active() const noexcept;

} }
返回值

如果 *this 在任何线程中处于活动状态,则为 true,否则为 false


print

#include <boost/leaf/context.hpp>
namespace boost { namespace leaf {

  template <class... E>
  void context<E...>::print( std::ostream & os ) const;

  template <class CharT, class Traits>
  friend std::ostream & context<E...>::operator<<( std::basic_ostream<CharT, Traits> &, context const & )
  {
      ctx.print(os);
      return os;
  }

} }
效果

打印当前存储在 *this 中的所有错误对象,以及每个单独错误对象关联的唯一错误 ID。


unload

#include <boost/leaf/context.hpp>
namespace boost { namespace leaf {

  template <class... E>
  void context<E...>::unload( error_id id ) noexcept;

} }
要求

!is_active().

效果

类型为 E 的每个存储的错误对象都会移动到调用堆栈中另一个活动的 context 对象中,如果该对象为类型为 E 的对象提供存储(如果有),否则会被丢弃。如果目标对象与指定的 id 关联,则不会覆盖它们,除非 id.value() == 0


context_activator

#include <boost/leaf/error.hpp>
namespace boost { namespace leaf {

  template <class Ctx>
  class context_activator
  {
    context_activator( context_activator const & ) = delete;
    context_activator & operator=( context_activator const & ) = delete;

  public:

    explicit context_activator( Ctx & ctx ) noexcept;
    context_activator( context_activator && ) noexcept;
    ~context_activator() noexcept;
  };

} }

context_activator 是一个简单的类,它使用 RAII 激活和停用 context

如果在初始化 context_activatorctx.is_active() 为 true,则构造函数和析构函数不起作用。否则

  • 构造函数在 *this 中存储对 ctx 的引用,并调用 ctx.activate()。

  • 析构函数

    • 如果 ctx.is_active()false,则不起作用(即,可以在 context_activator 对象过期之前手动调用 deactivate);

    • 否则,调用 ctx.deactivate()。

为了自动推导 Ctx,请使用 activate_context


diagnostic_details

#include <boost/leaf/diagnostics.hpp>
namespace boost { namespace leaf {

  class diagnostic_details: public error_info
  {
    //Constructors unspecified

    template <class CharT, class Traits>
    friend std::ostream & operator<<( std::basic_ostream<CharT, Traits> &, diagnostic_details const & );
  };

} }

传递给错误处理函数(如 try_handle_sometry_handle_alltry_catch)的处理程序可以接受类型为 diagnostic_details const & 的参数,如果它们需要打印有关错误的诊断信息。

operator<< 打印的消息包括由 error_info 打印的消息,后跟有关已传递给 LEAF(与错误关联)的错误对象的信息,这些错误对象在任何活动的 context 中都没有可用的存储(这些错误对象被 LEAF 丢弃,因为没有处理程序需要它们)。

附加信息包括所有此类错误对象的类型和值(但请参阅 show_in_diagnostics)。

diagnostic_details(和 diagnostic_info)的行为受宏 BOOST_LEAF_CFG_DIAGNOSTICS 的值影响

  • 如果它为 1(默认值),则 LEAF 生成 diagnostic_details,但仅当调用堆栈上的活动错误处理上下文接受类型为 diagnostic_details 的参数时才生成;

  • 如果它为 0,即使对于接受类型为 diagnostic_details 的参数的错误处理上下文,diagnostic_details 功能也会被存根化。这可能会在某些程序中节省错误路径上的一些周期(但可能不值得)。

使用 diagnostic_details 可能会动态分配内存,但仅当活动错误处理程序接受类型为 diagnostic_details 的参数时才会分配内存。

diagnostic_info

#include <boost/leaf/diagnostics.hpp>
namespace boost { namespace leaf {

  class diagnostic_info: public error_info
  {
    //Constructors unspecified

    template <class CharT, class Traits>
    friend std::ostream & operator<<( std::basic_ostream<CharT, Traits> &, diagnostic_info const & );
  };

} }

传递给 try_handle_sometry_handle_alltry_catch 的处理程序可以接受类型为 diagnostic_info const & 的参数,如果它们需要打印有关错误的诊断信息。

operator<< 打印的消息包括由 error_info 打印的消息,后跟有关已传递给 LEAF(与错误关联)的错误对象的基本信息,这些错误对象在任何活动的 context 中都没有可用的存储(这些错误对象被 LEAF 丢弃,因为没有处理程序需要它们)。

附加信息仅限于第一个此类错误对象的类型名称以及它们的总计数。

diagnostic_info(和 diagnostic_details)的行为受宏 BOOST_LEAF_CFG_DIAGNOSTICS 的值影响

  • 如果它为 1(默认值),则 LEAF 生成 diagnostic_info,但仅当调用堆栈上的活动错误处理上下文接受类型为 diagnostic_info 的参数时才生成;

  • 如果它为 0,即使对于接受类型为 diagnostic_info 的参数的错误处理上下文,diagnostic_info 功能也会被存根化。这可能会在某些程序中减少错误路径上的一些周期(但这可能不值得)。

error_id

#include <boost/leaf/error.hpp>
namespace boost { namespace leaf {

  class error_id
  {
  public:

    error_id() noexcept;

    template <class Enum>
    result( Enum e, typename std::enable_if<std::is_error_code_enum<Enum>::value, Enum>::type * = 0 ) noexcept;

    error_id( std::error_code const & ec ) noexcept;

    int value() const noexcept;
    explicit operator bool() const noexcept;

    std::error_code to_error_code() const noexcept;

    friend bool operator==( error_id a, error_id b ) noexcept;
    friend bool operator!=( error_id a, error_id b ) noexcept;
    friend bool operator<( error_id a, error_id b ) noexcept;

    template <class... Item>
    error_id load( Item && ... item ) const noexcept;

    template <class CharT, class Traits>
    friend std::ostream & operator<<( std::basic_ostream<CharT, Traits> &, error_id );
  };

  bool is_error_id( std::error_code const & ec ) noexcept;

  template <class... E>
  error_id new_error( E && ... e ) noexcept;

  error_id current_error() noexcept;

} }

error_id 类型的值标识整个程序中特定错误的发生。它们可以被复制、移动、赋值和与其他 error_id 对象进行比较。它们与 int 一样高效。


构造函数

#include <boost/leaf/error.hpp>
namespace boost { namespace leaf {

  error_id::error_id() noexcept = default;

  template <class Enum>
  error_id::error_id( Enum e, typename std::enable_if<std::is_error_code_enum<Enum>::value, Enum>::type * = 0 ) noexcept;

  error_id::error_id( std::error_code const & ec ) noexcept;

} }

默认初始化的 error_id 对象不代表特定错误。它与任何其他默认初始化的 error_id 对象比较相等。所有其他 error_id 对象都标识特定错误的发生。

当使用 error_id 类型的对象初始化 result<T> 对象时,即使传递默认初始化的 error_id 值,它也会在错误状态下初始化。

error_id 对象转换为 std::error_code 使用 LEAF 识别的未指定的 std::error_category。这允许 error_id 通过使用 std::error_code 的接口进行传输。std::error_code 构造函数允许恢复原始 error_id

要检查给定的 std::error_code 是否实际携带 error_id,请使用 is_error_id

通常,用户通过调用 new_error 创建新的 error_id 对象。接受 std::error_code 的构造函数和接受类型为 Enum 的构造函数(对于该类型,std::is_error_code_enum<Enum>::valuetrue)具有以下效果

  • 如果 ec.value()0,则效果与使用默认构造函数相同。

  • 否则,如果 is_error_id(ec)true,则原始 error_id 值用于初始化 *this

  • 否则,*thisnew_error 返回的值初始化,同时 ec 传递给 load,这使得与 try_handle_sometry_handle_alltry_catch 一起使用的处理程序可以将其接收为类型为 std::error_code 的参数。


is_error_id

#include <boost/leaf/error.hpp>
namespace boost { namespace leaf {

  bool is_error_id( std::error_code const & ec ) noexcept;

} }
返回值

如果 ec 使用 LEAF 特定的 std::error_category(将其标识为携带错误 ID 而不是其他错误代码),则返回 true;否则返回 false


load

#include <boost/leaf/error.hpp>
namespace boost { namespace leaf {

  template <class... Item>
  error_id error_id::load( Item && ... item ) const noexcept;

} }
要求

每个 Item…​ 类型都必须是 no-throw movable 的。

效果
  • 如果 this->value()==0,则所有 item…​ 都将被丢弃,并且不会采取进一步操作。

  • 否则,每个 item 的处理方式取决于其类型

    • 如果它是一个接受类型为 E & 的单个参数的函数,则使用当前与 *this 关联的类型为 E 的对象调用该函数。如果不存在此类对象,则默认初始化的对象与 *this 关联,然后传递给该函数。

    • 如果它是一个不接受任何参数的函数,则调用该函数以获取与 *this 关联的错误对象,但在 void 函数的特殊情况下,将调用它,并且不获取/加载错误对象。

    • 否则,item 本身被假定为错误对象,它与 *this 关联。

返回值

*this.

load 丢弃在任何活动错误处理调用范围内未使用的错误对象。
当加载到 context 中时,类型为 E 的错误对象将覆盖先前加载的类型为 E 的对象(如果有)。
另请参阅

加载错误对象.


operator==, !=, <

#include <boost/leaf/error.hpp>
namespace boost { namespace leaf {

  friend bool operator==( error_id a, error_id b ) noexcept;
  friend bool operator!=( error_id a, error_id b ) noexcept;
  friend bool operator<( error_id a, error_id b ) noexcept;

} }

这些函数具有通常的语义,比较 a.value()b.value()

未指定由 operator< 实现的确切严格弱排序。特别是,如果对于两个 error_id 对象 aba < b 为真,则不一定表示由 a 标识的失败早于由 b 标识的失败发生。

operator bool

#include <boost/leaf/error.hpp>
namespace boost { namespace leaf {

    explicit error_id::operator bool() const noexcept;

} }
效果

如同 return value()!=0


to_error_code

#include <boost/leaf/error.hpp>
namespace boost { namespace leaf {

    std::error_code error_id::to_error_code() const noexcept;

} }
效果

返回一个 std::error_code,其 value()*this 相同,使用未指定的 std::error_category

返回的对象可用于初始化 error_id,在这种情况下,将恢复原始 error_id 值。
使用 is_error_id 检查给定的 std::error_code 是否携带 error_id

value

#include <boost/leaf/error.hpp>
namespace boost { namespace leaf {

    int error_id::value() const noexcept;

} }
效果
  • 如果 *this 是使用默认构造函数初始化的,则返回 0。

  • 否则返回一个保证不为 0 的 int:程序范围内的唯一失败标识符。


error_monitor

#include <boost/leaf/on_error.hpp>
namespace boost { namespace leaf {

  class error_monitor
  {
  public:

    error_monitor() noexcept;

    error_id check() const noexcept;

    error_id assigned_error_id( E && ... e ) const noexcept;
  };

} }

当通过不使用 LEAF 报告错误的非协作 API 增强使用 LEAF 传递的失败时(因此在错误时不会返回 error_id),此类有助于获取 error_id 以关联错误对象。

此类的一般用法如下

error_code compute_value( int * out_value ) noexcept; (1)

leaf::error<int> augmenter() noexcept
{
  leaf::error_monitor cur_err; (2)

  int val;
  auto ec = compute_value(&val);

  if( failure(ec) )
    return cur_err.assigned_error_id().load(e1, e2, ...); (3)
  else
    return val; (4)
}
1 非协作第三方 API 不使用 LEAF,但可能会导致调用使用 LEAF 的用户回调。如果我们的回调报告失败,我们将使用调用范围内可用的错误对象来增强它,即使 compute_value 无法传递 error_id
2 初始化 error_monitor 对象。
3 compute_value 的调用失败
  • 如果在初始化 augment 对象后(由调用线程)调用了 new_error,则 assigned_error_id 返回由 new_error 返回的最后一个 error_id。如果失败源自我们的回调(由 compute_value 内部调用),则会发生这种情况。

  • 否则,assigned_error_id 调用 new_error 并返回该 error_id

4 调用成功,返回计算值。

check 函数的工作方式类似,但它不是调用 new_error,而是返回默认初始化的 error_id


e_api_function

#include <boost/leaf/common.hpp>
namespace boost { namespace leaf {

  struct e_api_function {char const * value;};

} }

e_api_function 类型旨在捕获失败的 API 函数的名称。例如,如果您要从 fread 报告错误,则可以使用 leaf::e_api_function {"fread"}

传递的值存储为 C 字符串 (char const *),因此 value 应该仅使用字符串字面量初始化。

e_at_line

#include <boost/leaf/common.hpp>
namespace boost { namespace leaf {

  struct e_at_line { int value; };

} }

e_at_line 可用于在报告有关文本文件(例如解析错误)的错误时传递行号。


e_errno

#include <boost/leaf/common.hpp>
namespace boost { namespace leaf {

  struct e_errno
  {
    int value;
    explicit e_errno(int value=errno);

    template <class CharT, class Traits>
    friend std::ostream & operator<<( std::basic_ostream<CharT, Traits> &, e_errno const & );
  };

} }

默认情况下,构造函数使用 errno 初始化 value,但调用者可以传递特定的错误代码。当在自动生成的诊断消息中打印时,e_errno 对象使用 strerror 将错误代码转换为字符串。


e_file_name

#include <boost/leaf/common.hpp>
namespace boost { namespace leaf {

  struct e_file_name { std::string value; };

} }

当文件操作失败时,您可以使用 e_file_name 来存储文件名。

最好定义您自己的文件名包装器,以避免如果不同的模块都使用 leaf::e_file_name 而发生冲突。最好使用描述性名称来阐明它是哪种类型的文件名(例如 e_source_file_namee_destination_file_name),或者至少在给定模块的命名空间中定义 e_file_name

e_LastError

#include <boost/leaf/common.hpp>
namespace boost { namespace leaf {

  namespace windows
  {
    struct e_LastError
    {
      unsigned value;

      explicit e_LastError(unsigned value);

#if BOOST_LEAF_CFG_WIN32
      e_LastError();

      template <class CharT, class Traits>
      friend std::ostream & operator<<( std::basic_ostream<CharT, Traits> &, e_LastError const & );
#endif
    };
  }

} }

e_LastError 旨在传递 Windows 上的 GetLastError() 值。默认构造函数通过 GetLastError() 初始化 value。请参阅 配置


e_source_location

#include <boost/leaf/error.hpp>
namespace boost { namespace leaf {

  struct e_source_location
  {
    char const * file;
    int line;
    char const * function;

    template <class CharT, class Traits>
    friend std::ostream & operator<<( std::basic_ostream<CharT, Traits> &, e_source_location const & );
  };

} }

BOOST_LEAF_NEW_ERRORBOOST_LEAF_THROW_EXCEPTION 宏将 __FILE____LINE____FUNCTION__ 捕获到 e_source_location 对象中。


e_type_info_name

#include <boost/leaf/common.hpp>
namespace boost { namespace leaf {

  struct e_type_info_name { char const * value; };

} }

e_type_info_name 旨在存储 std::type_info::name 的返回值。


error_info

#include <boost/leaf/handle_errors.hpp>
namespace boost { namespace leaf {

  class error_info
  {
    //Constructors unspecified

  public:

    error_id error() const noexcept;

    bool exception_caught() const noexcept;
    std::exception const * exception() const noexcept;

    template <class CharT, class Traits>
    friend std::ostream & operator<<( std::basic_ostream<CharT, Traits> &, error_info const & );
  };

} }

传递给错误处理函数(如 try_handle_sometry_handle_alltry_catch)的处理程序可以接受类型为 error_info const & 的参数,以接收有关正在处理的错误的一般信息。

error 成员函数返回程序范围内唯一的错误 error_id

exception_caught 成员函数返回 true,如果接收到 *this 的处理程序正在被调用以处理异常,否则返回 false

如果正在处理异常,则 exception 成员函数返回指向捕获异常的 std::exception 子对象的指针,如果无法将该异常转换为 std::exception,则返回 0

除非 exception_caught()true,否则调用 exception 成员函数是非法的。

operator<< 重载打印有关当前存储在 context 中的每个错误对象的诊断信息,该 context 本地于调用处理程序的 try_handle_sometry_handle_alltry_catch 作用域,但仅当它与由 error() 返回的 error_id 关联时才打印。


result

#include <boost/leaf/result.hpp>
namespace boost { namespace leaf {

  template <class T>
  class result
  {
  public:

    using value_type = T;

    // NOTE: Copy constructor implicitly deleted.
    result( result && r ) noexcept;

    template <class U, class = typename std::enable_if<std::is_convertible<U, T>::value>::type>
    result( result<U> && r ) noexcept;

    result() noexcept;

    result( T && v ) noexcept;

    result( T const & v );

    result( error_id err ) noexcept;

    template <class U, class = typename std::enable_if<std::is_convertible<U, T>::value>::type>
    result( U && u );

#if BOOST_LEAF_CFG_STD_SYSTEM_ERROR

    result( std::error_code const & ec ) noexcept;

    template <class Enum, class = typename std::enable_if<std::is_error_code_enum<Enum>::value, int>::type>
    result( Enum e ) noexcept;

#endif

    // NOTE: Assignment operator implicitly deleted.
    result & operator=( result && r ) noexcept;

    template <class U, class = typename std::enable_if<std::is_convertible<U, T>::value>::type>
    result & operator=( result<U> && r ) noexcept;

    bool has_value() const noexcept;
    bool has_error() const noexcept;
    explicit operator bool() const noexcept;

    T const & value() const &;
    T & value() &;
    T const && value() const &&;
    T && value() &&;

    T const * operator->() const noexcept;
    T * operator->() noexcept;

    T const & operator*() const & noexcept;
    T & operator*() & noexcept;
    T const && operator*() const && noexcept;
    T && operator*() && noexcept;

    <<unspecified-type>> error() noexcept;

    template <class... Item>
    error_id load( Item && ... item ) noexcept;

    void unload();

    template <class CharT, class Traits>
    friend std::ostream & operator<<( std::basic_ostream<CharT, Traits> &, result const & );
  };

  template <>
  class result<void>
  {
  public:

    using value_type = void;

    // NOTE: Copy constructor implicitly deleted.
    result( result && r ) noexcept;

    result() noexcept;

    result( error_id err ) noexcept;

#if BOOST_LEAF_CFG_STD_SYSTEM_ERROR

    result( std::error_code const & ec ) noexcept;

    template <class Enum, typename std::enable_if<std::is_error_code_enum<Enum>::value, Enum>::type>
    result( Enum e ) noexcept;

#endif

    // NOTE: Assignment operator implicitly deleted.
    result & operator=( result && r ) noexcept;

    explicit operator bool() const noexcept;

    void value() const;

    void const * operator->() const noexcept;
    void * operator->() noexcept;

    void operator*() const noexcept;

    <<unspecified-type>> error() noexcept;

    template <class... Item>
    error_id load( Item && ... item ) noexcept;

    void unload();

    template <class CharT, class Traits>
    friend std::ostream & operator<<( std::basic_ostream<CharT, Traits> &, result const &);
  };

  struct bad_result: std::exception { };

  template <class T>
  struct is_result_type<result<T>>: std::true_type
  {
  };

} }

result<T> 类型可以由生成类型为 T 的值但可能无法执行此操作的函数返回。

要求

T 必须是可移动的,并且其移动构造函数可能不会抛出异常。

不变式

result<T> 对象处于以下三种状态之一

  • 值状态,在这种状态下,它包含类型为 T 的对象,并且可以使用 value / operator* / operator-> 访问包含的值。

  • 错误状态,在这种状态下,它包含一个错误 ID,并且调用 value 会抛出 leaf::bad_result

  • 动态捕获状态,它与错误状态相同,但除了错误 ID 之外,它还保存动态捕获的错误对象列表;请参阅 try_capture_all

result<T> 对象是不可抛出移动的,但不可复制。


构造函数

#include <boost/leaf/result.hpp>
namespace boost { namespace leaf {

    // NOTE: Copy constructor implicitly deleted.

    template <class T>
    result<T>::result( result && r ) noexcept;

    template <class T>
    template <class U, class = typename std::enable_if<std::is_convertible<U, T>::value>::type>
    result<T>::result( result<U> && r ) noexcept;

    template <class T>
    result<T>::result() noexcept;

    template <class T>
    result<T>::result( T && v ) noexcept;

    template <class T>
    result<T>::result( T const & v );

    template <class T>
    result<T>::result( error_id err ) noexcept;

    template <class T>
    template <class U, class = typename std::enable_if<std::is_convertible<U, T>::value>::type>
    result<T>::result( U && u );

#if BOOST_LEAF_CFG_STD_SYSTEM_ERROR

    template <class T>
    result<T>::result( std::error_code const & ec ) noexcept;

    template <class T>
    template <class Enum, class = typename std::enable_if<std::is_error_code_enum<Enum>::value, int>::type>
    result<T>::result( Enum e ) noexcept;

#endif

} }
要求

T 必须是可移动的,并且其移动构造函数可能不会抛出异常;或 void

效果

建立 result<T> 不变式

  • 要获取 值状态 中的 result<T>,请使用类型为 T 的对象或使用默认构造函数初始化它。

  • 要获取 错误状态 中的 result<T>,请使用以下内容初始化它

    • 一个 error_id 对象。

      使用默认初始化的 error_id 对象(.value() 返回 0)初始化 result<T> 仍然会导致 错误状态
    • 一个 std::error_code 对象。

    • 类型为 Enum 的对象,对于该类型,std::is_error_code_enum<Enum>::valuetrue

  • 要获取 动态捕获状态 中的 result<T>,请调用 try_capture_all

当使用 std::error_code 对象初始化 result 对象时,它用于初始化 error_id 对象,然后行为与使用 error_id 初始化时相同。

抛出
  • 在值状态下初始化 result<T> 可能会抛出异常,具体取决于调用 T 的哪个构造函数;

  • 其他构造函数不会抛出异常。

处于值状态的 result 在布尔上下文中转换为 true。未处于值状态的 result 在布尔上下文中转换为 false
result<T> 对象是不可抛出移动的,但不可复制。

error

#include <boost/leaf/result.hpp>
namespace boost { namespace leaf {

  template <class... E>
  <<unspecified-type>> result<T>::error() noexcept;

} }

返回:未指定类型的代理对象,隐式可转换为 result 类模板的任何实例,以及 error_id

  • 如果代理对象转换为某个 result<U>

    • 如果 *this 处于 值状态,则返回 result<U>(error_id())

    • 否则,*this 的状态将移动到返回的 result<U> 中。

  • 如果代理对象转换为 error_id

    • 如果 *this 处于 值状态,则返回默认初始化的 error_id 对象。

    • 如果 *this 处于 错误捕获状态,则所有捕获的错误对象都将 加载 到调用线程中,并返回捕获的 error_id 值。

    • 如果 *this 处于 错误状态,则返回存储的 error_id

  • 如果未使用代理对象,则不会修改 *this 的状态。

返回的代理对象引用 *this;避免持有它。

load

#include <boost/leaf/result.hpp>
namespace boost { namespace leaf {

  template <class T>
  template <class... Item>
  error_id result<T>::load( Item && ... item ) noexcept;

} }

此成员函数旨在用于返回 result<T> 的函数中的 return 语句,以将其他错误对象转发给调用者。

效果

如同 error_id(this->error()).load(std::forward<Item>(item)…​)

返回值

*this.


operator=

#include <boost/leaf/result.hpp>
namespace boost { namespace leaf {

  template <class T>
  result<T> & result<T>::operator=( result && ) noexcept;

  template <class T>
  template <class U>
  result<T> & result<T>::operator=( result<U> && ) noexcept;

} }
效果

销毁 *this,然后重新初始化它,就像使用适当的 result<T> 构造函数一样。基本异常安全保证。


has_value

#include <boost/leaf/result.hpp>
namespace boost { namespace leaf {

  template <class T>
  bool result<T>::has_value() const noexcept;

} }
返回值

如果 *this 处于 值状态,则返回 true,否则返回 false


has_error

#include <boost/leaf/result.hpp>
namespace boost { namespace leaf {

  template <class T>
  bool result<T>::has_error() const noexcept;

} }
返回值

如果 *this 处于 值状态,则返回 false,否则返回 true


operator bool

#include <boost/leaf/result.hpp>
namespace boost { namespace leaf {

  template <class T>
  result<T>::operator bool() const noexcept;

} }
返回值

如果 *this 处于 值状态,则返回 true,否则返回 false


value

#include <boost/leaf/result.hpp>
namespace boost { namespace leaf {

  void result<void>::value() const;

  template <class T>
  T const & result<T>::value() const;

  template <class T>
  T & result<T>::value();

  struct bad_result: std::exception { };

} }

效果

  • 如果 *this 处于 值状态,则返回对存储值的引用。

  • 如果 *this 处于 动态捕获状态,则卸载捕获的错误对象,并且

  • 如果 *this 处于任何其他状态,则行为等同于 throw_exception(bad_result{})


value_type

result<T> 的成员类型,定义为 T 的同义词。


效果

如果 *this 处于 值状态,则返回对存储值的引用,否则抛出 bad_result


operator->

#include <boost/leaf/result.hpp>
namespace boost { namespace leaf {

  template <class T>
  T const * result<T>::operator->() const noexcept;

  template <class T>
  T * result<T>::operator->() noexcept;

} }
返回值

如果 *this 处于 值状态,则返回指向存储值的指针;否则返回 0。


operator*

#include <boost/leaf/result.hpp>
namespace boost { namespace leaf {

  template <class T>
  T const & result<T>::operator*() const noexcept;

  template <class T>
  T & result<T>::operator*() noexcept;

} }
要求

*this 必须处于 值状态

返回值

对存储值的引用。


show_in_diagnostics

#include <boost/leaf/handle_errors.hpp>
namespace boost { namespace leaf {

template <class E>
struct show_in_diagnostics: std::true_type
{
};

} }

可以专门化此模板,以防止敏感类型的错误对象出现在自动生成的诊断消息中。示例

struct e_user_name
{
  std::string value;
};

namespace boost { namespace leaf {

  template <>
  struct show_in_diagnostics<e_user_name>: std::false_type
  {
  };

} }

参考:谓词

每个“参考”部分的内容均按字母顺序组织。

谓词是一种特殊的错误处理程序参数类型,它使 处理程序选择过程 可以考虑可用错误对象的,而不仅仅是它们的类型;请参阅 使用谓词处理错误

以下谓词可用

此外,任何用户定义的类型 Pred,只要 is_predicate<Pred>::valuetrue,都将被视为谓词。在这种情况下,需要满足以下条件:

  • Pred 定义一个可访问的成员类型 error_type,以指定其需要的错误对象类型;

  • Pred 定义一个可访问的静态成员函数 evaluate,该函数返回布尔类型,并且可以使用类型为 error_type const & 的对象调用;

  • Pred 实例可以使用类型为 error_type 的对象进行初始化。

当错误处理程序接受谓词类型 Pred 的参数时,如果类型为 Pred::error_type 的错误对象 e 不可用,则 处理程序选择过程 将丢弃该处理程序。否则,如果 Pred::evaluate(e) 返回 false,则也会丢弃该处理程序。如果调用了处理程序,则使用 Pred{e} 初始化 Pred 参数。

谓词在错误处理程序被调用之前进行评估,因此它们可能无法访问动态状态(当然,错误处理程序本身可以访问动态状态,例如通过 lambda 表达式捕获)。
示例 1
enum class my_error { e1 = 1, e2, e3 };

struct my_pred
{
  using error_type = my_error; (1)

  static bool evaluate(my_error) noexcept; (2)

  my_error matched; (3)
}

namespace boost { namespace leaf {

  template <>
  struct is_predicate<my_pred>: std::true_type
  {
  };

} }
1 此谓词需要类型为 my_error 的错误对象。
2 处理程序选择过程将使用类型为 my_error 的对象 e 调用此函数,以评估谓词……​
3 ……​如果成功,则使用 my_pred{e} 初始化 my_pred 错误处理程序参数。
示例 2
struct my_pred
{
  using error_type = leaf::e_errno; (1)

  static bool evaluate(leaf::e_errno const &) noexcept; (2)

  leaf::e_errno const & matched; (3)
}

namespace boost { namespace leaf {

  template <>
  struct is_predicate<my_pred>: std::true_type
  {
  };

} }
1 此谓词需要类型为 e_errno 的错误对象。
2 处理程序选择过程将使用类型为 e_errno 的对象 e 调用此函数,以评估谓词……​
3 ……​如果成功,则使用 my_pred{e} 初始化 my_pred 错误处理程序参数。

catch_

#include <boost/leaf/pred.hpp>
namespace boost { namespace leaf {

  template <class... Ex>
  struct catch_
  {
    std::exception const & matched;

    // Other members not specified
  };

  template <class Ex>
  struct catch_<Ex>
  {
    Ex const & matched;

    // Other members not specified
  };

  template <class... Ex>
  struct is_predicate<catch_<Ex...>>: std::true_type
  {
  };

} }

当错误处理程序接受类型为 catch_ 模板实例的参数时,处理程序选择过程 首先检查是否捕获了 std::exception。如果没有,则丢弃该处理程序。否则,如果捕获的 std::exception 无法 dynamic_cast 为任何指定的类型 Ex…​,则也会丢弃该处理程序。

如果调用了错误处理程序,则可以使用 matched 成员访问异常对象。

另请参阅:使用谓词处理错误
虽然 catch_ 要求捕获的异常对象是派生自 std::exception 的类型,但并不要求 Ex…​ 类型派生自 std::exception
示例 1
struct ex1: std::exception { };
struct ex2: std::exception { };

leaf::try_catch(

  []
  {
    return f(); // throws
  },

  [](leaf::catch_<ex1, ex2> c)
  { (1)
    assert(dynamic_cast<ex1 const *>(&c.matched) || dynamic_cast<ex2 const *>(&c.matched));
    ....
  } );
1 如果 f 抛出类型为 ex1ex2 的异常,则选择该处理程序。
示例 2
struct ex1: std::exception { };

leaf::try_handle_some(

  []
  {
    return f(); // returns leaf::result<T>
  },

  [](ex1 & e)
  { (1)
    ....
  } );
1 如果 f 抛出类型为 ex1 的异常,则选择该处理程序。请注意,如果我们只对一种异常类型感兴趣,只要该类型派生自 std::exception,就不需要使用 catch_

if_not

#include <boost/leaf/pred.hpp>
namespace boost { namespace leaf {

  template <class P>
  struct if_not
  {
    <<deduced>> matched;

    // Other members not specified
  };

  template <class P>
  struct is_predicate<if_not<P>>: std::true_type
  {
  };

} }

当错误处理程序接受类型为 if_not<P> 的参数时,其中 P 是另一种谓词类型,处理程序选择过程 首先检查类型为 P 所需的 E 的错误对象是否可用。如果不可用,则丢弃该处理程序。否则,如果 P 评估为 true,则也会丢弃该处理程序。

如果调用了错误处理程序,则可以使用 matched 访问匹配的对象 E

另请参阅 使用谓词处理错误
示例
enum class my_enum { e1, e2, e3 };

leaf::try_handle_some(

  []
  {
    return f(); // returns leaf::result<T>
  },

  []( leaf::if_not<leaf::match<my_enum, my_enum::e1, my_enum::e2>> )
  { (1)
    ....
  } );
1 如果类型为 my_enum 的对象,它与 e1e2 比较 相等,并且 检测到的错误相关联,则选择该处理程序。

match

#include <boost/leaf/pred.hpp>
namespace boost { namespace leaf {

  template <class E, auto... V>
  class match
  {
    <<deduced>> matched;

    // Other members not specified
  };

  template <class E, auto... V>
  struct is_predicate<match<E, V...>>: std::true_type
  {
  };

} }

当错误处理程序接受类型为 match<E, V…​> 的参数时,处理程序选择过程 首先检查类型为 E 的错误对象 e 是否可用。如果不可用,则丢弃该处理程序。否则,如果未满足以下条件,则也会丢弃该处理程序

p1 || p2 || …​ pn.

其中 pi 等效于 e == Vi,除非 Vi 是指向函数的指针

bool (*Vi)(T x).

在这种情况下,需要 Vi != 0 并且 x 可以使用 E const & 初始化,然后 pi 等效于

Vi(e).

特别是,对于任何 Vi,传递指向函数 leaf::category<Enum> 的指针是有效的,其中

std::is_error_code_enum<Enum>::value || std::is_error_condition_enum<Enum>::value.

在这种情况下,pi 等效于

&e.category() == &std::error_code(Enum{}).category().

如果调用了错误处理程序,则可以使用 matched 访问 e

另请参阅 使用谓词处理错误
示例 1:处理枚举值的子集。
enum class my_enum { e1, e2, e3 };

leaf::try_handle_some(

  []
  {
    return f(); // returns leaf::result<T>
  },

  []( leaf::match<my_enum, my_enum::e1, my_enum::e2> m )
  { (1)
    static_assert(std::is_same<my_enum, decltype(m.matched)>::value);
    assert(m.matched == my_enum::e1 || m.matched == my_enum::e2);
    ....
  } );
1 如果类型为 my_enum 的对象,它与 e1e2 比较相等,并且与检测到的错误相关联,则选择该处理程序。
示例 2:处理 std::error_code 枚举值的子集(至少需要 C++17,有关 C++11 兼容的解决方法,请参见示例 4)。
enum class my_enum { e1=1, e2, e3 };

namespace std
{
  template <> struct is_error_code_enum<my_enum>: std::true_type { };
}

leaf::try_handle_some(

  []
  {
    return f(); // returns leaf::result<T>
  },

  []( leaf::match<std::error_code, my_enum::e1, my_enum::e2> m )
  { (1)
    static_assert(std::is_same<std::error_code const &, decltype(m.matched)>::value);
    assert(m.matched == my_enum::e1 || m.matched == my_enum::e2);
    ....
  } );
1 如果类型为 std::error_code 的对象,它与 e1e2 比较相等,并且与检测到的错误相关联,则选择该处理程序。
示例 3:处理特定的 std::error_code::category(至少需要 C++17)。
enum class enum_a { a1=1, a2, a3 };
enum class enum_b { b1=1, b2, b3 };

namespace std
{
  template <> struct is_error_code_enum<enum_a>: std::true_type { };
  template <> struct is_error_code_enum<enum_b>: std::true_type { };
}

leaf::try_handle_some(

  []
  {
    return f(); // returns leaf::result<T>
  },

  []( leaf::match<std::error_code, leaf::category<enum_a>, enum_b::b2> m )
  { (1)
    static_assert(std::is_same<std::error_code const &, decltype(m.matched)>::value);
    assert(&m.matched.category() == &std::error_code(enum_{}).category() || m.matched == enum_b::b2);
    ....
  } );
1 如果类型为 std::error_code 的对象,它要么具有与 enum_a 相同的 std::error_category,要么与 enum_b::b2 比较相等,并且与检测到的错误相关联,则选择该处理程序。

leaf::category 模板的使用需要自动推导每个 Vi 的类型,这反过来又需要 C++17 或更高版本。将 std::error_code 用作 E 也是如此,但 LEAF 为这种情况提供了兼容 C++11 的解决方法,使用模板 condition。以下内容等效于示例 2

示例 4:使用 C++11 兼容的 API 处理 std::error_code 枚举值的子集。
enum class my_enum { e1=1, e2, e3 };

namespace std
{
  template <> struct is_error_code_enum<my_enum>: std::true_type { };
}

leaf::try_handle_some(

  []
  {
    return f(); // returns leaf::result<T>
  },

  []( leaf::match<leaf::condition<my_enum>, my_enum::e1, my_enum::e2> m )
  {
    static_assert(std::is_same<std::error_code const &, decltype(m.matched)>::value);
    assert(m.matched == my_enum::e1 || m.matched == my_enum::e2);
    ....
  } );

match 模板可以接受一组值,也可以接受指向实现自定义比较的函数的指针。在以下示例中,我们定义了一个处理程序,该处理程序将被选中以处理任何错误,该错误传递的用户定义类型为 severity 的对象的值大于 4

示例 5:处理 severity::value 大于指定阈值的故障(至少需要 C++17)。
struct severity { int value; }

template <int S>
constexpr bool severity_greater_than( severity const & e ) noexcept
{
  return e.value > S;
}

leaf::try_handle_some(

  []
  {
    return f(); // returns leaf::result<T>
  },

  []( leaf::match<severity, severity_greater_than<4>> m )
  {
    static_assert(std::is_same<severity const &, decltype(m.matched)>::value);
    assert(m.matched.value > 4);
    ....
  } );

match_member

#include <boost/leaf/pred.hpp>
namespace boost { namespace leaf {

  template <auto, auto... V>
  struct match_member;

  template <class E, class T, T E::* P, auto... V>
  struct match_member<P, V...>
  {
    E const & matched;

    // Other members not specified
  };

  template <auto P, auto... V>
  struct is_predicate<match_member<P, V...>>: std::true_type
  {
  };

} }

此谓词类似于 match_value,但能够绑定 E 的任何可访问数据成员;例如,match_member<&E::value, V…​> 等效于 match_value<E, V…​>

另请参阅 使用谓词处理错误
match_member 至少需要 C++17,而 match_value 则不需要。

match_value

#include <boost/leaf/pred.hpp>
namespace boost { namespace leaf {

  template <class E, auto... V>
  struct match_value
  {
    E const & matched;

    // Other members not specified
  };

  template <class E, auto... V>
  struct is_predicate<match_value<E, V...>>: std::true_type
  {
  };

} }

此谓词类似于 match,但 match 将可用的类型为 E 的错误对象 e 与指定的值 V…​ 进行比较,而 match_value 则使用 e.value

另请参阅 使用谓词处理错误
示例
struct e_errno { int value; }

leaf::try_handle_some(

  []
  {
    return f(); // returns leaf::result<T>
  },

  []( leaf::match_value<e_errno, ENOENT> m )
  { (1)
    static_assert(std::is_same<e_errno const &, decltype(m.matched)>::value);
    assert(m.matched.value == ENOENT);
    ....
  } );
1 如果类型为 e_errno 的对象,其 .value 等于 ENOENT,并且与检测到的错误相关联,则选择该处理程序。

参考:特性

每个“参考”部分的内容均按字母顺序组织。

is_predicate

#include <boost/leaf/pred.hpp>>
namespace boost { namespace leaf {

  template <class T>
  struct is_predicate: std::false_type
  {
  };

} }

is_predicate 模板由 处理程序选择过程 使用,以检测谓词类型。请参阅 使用谓词处理错误


is_result_type

#include <boost/leaf/error.hpp>>
namespace boost { namespace leaf {

  template <class R>
  struct is_result_type: std::false_type
  {
  };

} }

try_handle_sometry_handle_all 提供的错误处理功能 — 包括 加载 任意类型的错误对象的能力 — 与任何外部 result<T> 类型 R 兼容,只要对于给定类型为 R 的对象 r 满足以下条件:

  • 如果 bool(r)true,则 r 表示成功,在这种情况下,调用 r.value() 以恢复 T 值是有效的。

  • 否则,r 表示失败,在这种情况下,调用 r.error() 是有效的。返回的值用于初始化 error_id(注意:error_id 可以通过 std::error_code 初始化)。

要使用外部 result<T> 类型 R,您必须特化 is_result_type 模板,以便 is_result_type<R>::value 的评估结果为 true

当然,提供的 leaf::result<T> 类模板满足这些要求。此外,它允许使用 try_capture_all 在线程边界之间传输错误对象。

参考:宏

每个“参考”部分的内容均按字母顺序组织。

BOOST_LEAF_ASSIGN

#include <boost/leaf/error.hpp>
#define BOOST_LEAF_ASSIGN(v, r)\
  auto && <<temp>> = r;\
  if( !<<temp>> )\
    return <<temp>>.error();\
  v = std::forward<decltype(<<temp>>)>(<<temp>>).value()

当调用返回 result<T>(而不是 result<void>)的函数时,如果期望的行为是将任何错误原封不动地转发给调用方,则 BOOST_LEAF_ASSIGN 非常有用。

如果成功,类型为 T 的结果 value() 将分配给指定的变量 v,该变量必须在调用 BOOST_LEAF_ASSIGN 之前声明。但是,可以使用 BOOST_LEAF_ASSIGN 声明新变量,方法是在 v 中传递其类型及其名称,例如 BOOST_LEAF_ASSIGN(auto && x, f()) 调用 f,将错误转发给调用方,同时将成功的值捕获到 x 中。

另请参阅 BOOST_LEAF_AUTO

BOOST_LEAF_AUTO

#include <boost/leaf/error.hpp>
#define BOOST_LEAF_AUTO(v, r)\
  BOOST_LEAF_ASSIGN(auto v, r)

当调用返回 result<T>(而不是 result<void>)的函数时,如果期望的行为是将任何错误原封不动地转发给调用方,则 BOOST_LEAF_AUTO 非常有用。

示例
leaf::result<int> compute_value();

leaf::result<float> add_values()
{
  BOOST_LEAF_AUTO(v1, compute_value()); (1)
  BOOST_LEAF_AUTO(v2, compute_value()); (2)
  return v1 + v2;
}
1 调用 compute_value,在失败时跳出,在成功时定义一个局部变量 v1
2 再次调用 compute_value,在失败时跳出,在成功时定义一个局部变量 v2

当然,我们可以不使用 BOOST_LEAF_AUTO 编写 add_value。这等效于

leaf::result<float> add_values()
{
  auto v1 = compute_value();
  if( !v1 )
    return v1.error();

  auto v2 = compute_value();
  if( !v2 )
    return v2.error();

  return v1.value() + v2.value();
}
另请参阅 BOOST_LEAF_ASSIGN

BOOST_LEAF_CHECK

#include <boost/leaf/error.hpp>
#if BOOST_LEAF_CFG_GNUC_STMTEXPR

#define BOOST_LEAF_CHECK(r)\
  ({\
    auto && <<temp>> = (r);\
    if( !<<temp>> )\
      return <<temp>>.error();\
    std::move(<<temp>>);\
  }).value()

#else

#define BOOST_LEAF_CHECK(r)\
  {\
    auto && <<temp>> = (r);\
    if( !<<temp>> )\
      return <<temp>>.error();\
  }

#endif

当调用返回 result<void> 的函数时,如果期望的行为是将任何错误原封不动地转发给调用方,则 BOOST_LEAF_CHECK 非常有用。

示例
leaf::result<void> send_message( char const * msg );

leaf::result<int> compute_value();

leaf::result<int> say_hello_and_compute_value()
{
  BOOST_LEAF_CHECK(send_message("Hello!")); (1)
  return compute_value();
}
1 尝试发送消息,然后计算值,使用 BOOST_LEAF_CHECK 报告错误。

不使用 BOOST_LEAF_CHECK 的等效实现

leaf::result<float> add_values()
{
  auto r = send_message("Hello!");
  if( !r )
    return r.error();

  return compute_value();
}

如果 BOOST_LEAF_CFG_GNUC_STMTEXPR1(在 __GNUC__ 下这是默认值),则 BOOST_LEAF_CHECK 扩展为 GNU C 语句表达式,这允许将其与任何表达式中的非 void 结果类型一起使用;请参阅 检查错误


BOOST_LEAF_THROW_EXCEPTION

#include <boost/leaf/exception.hpp>
#define BOOST_LEAF_THROW_EXCEPTION <<exact-dedfinition-unspecified>>
效果

BOOST_LEAF_THROW_EXCEPTION(e…​) 等效于 leaf::throw_exception(e…​),只是当前源位置会自动与抛出的异常一起传递,位于 e_source_location 对象中(除了所有 e…​ 对象之外)。


BOOST_LEAF_NEW_ERROR

#include <boost/leaf/error.hpp>
#define BOOST_LEAF_NEW_ERROR <<exact-definition-unspecified>>
效果

BOOST_LEAF_NEW_ERROR(e…​) 等效于 leaf::new_error(e…​),只是当前源位置会自动传递,位于 e_source_location 对象中(除了所有 e…​ 对象之外)。

配置

识别以下配置宏

  • BOOST_LEAF_CFG_DIAGNOSTICS:将此宏定义为 0 会桩化 diagnostic_infodiagnostic_details(如果宏保持未定义,则 LEAF 将其定义为 1)。

  • BOOST_LEAF_CFG_STD_SYSTEM_ERROR:将此宏定义为 0 会禁用 std::error_code / std::error_condition 集成。在这种情况下,LEAF 不会 #include <system_error>,这对于嵌入式平台来说可能太重了(如果宏保持未定义,则 LEAF 将其定义为 1)。

  • BOOST_LEAF_CFG_STD_STRING:将此宏定义为 0 会禁用所有 std::string 的使用(这也需要 BOOST_LEAF_CFG_DIAGNOSTICS=0)。在这种情况下,LEAF 不会 #include <string>,这对于嵌入式平台来说可能太重了(如果宏保持未定义,则 LEAF 将其定义为 1)。

  • BOOST_LEAF_CFG_CAPTURE:将此宏定义为 0 会禁用 try_capture_all,它(仅在使用时)会动态分配内存(如果宏保持未定义,则 LEAF 将其定义为 1)。

  • BOOST_LEAF_CFG_GNUC_STMTEXPR:此宏控制是否根据 GNU C 语句表达式 定义 BOOST_LEAF_CHECK,这使其能够用于检查错误,类似于问号运算符在某些语言中的工作方式(请参阅 检查错误)。默认情况下,在 __GNUC__ 下宏定义为 1,否则定义为 0

  • BOOST_LEAF_CFG_WIN32:将此宏定义为 1 会启用 e_LastError 中的默认构造函数,以及在打印 diagnostic_details 时自动转换为字符串(通过 FormatMessageA)。如果宏保持未定义,则 LEAF 将其定义为 0(即使在 Windows 上,因为通常不希望包含 windows.h)。请注意,e_LastError 类型本身在所有平台上都可用,使用它的错误处理程序无需条件编译。

  • BOOST_LEAF_NO_EXCEPTIONS:禁用所有异常处理支持。如果保持未定义,LEAF 会根据编译器配置(例如 -fno-exceptions)自动定义它。

  • BOOST_LEAF_NO_THREADS:禁用 LEAF 中的所有线程安全。

配置 TLS 访问

LEAF 需要支持线程本地 void 指针。默认情况下,这是通过 C++11 thread_local 关键字实现的,但为了支持 嵌入式平台,可以将 LEAF 配置为使用线程本地指针数组,方法是定义 BOOST_LEAF_USE_TLS_ARRAY。在这种情况下,用户需要定义以下两个函数来实现所需的 TLS 访问

namespace boost { namespace leaf {

namespace tls
{
    void * read_void_ptr( int tls_index ) noexcept;
    void write_void_ptr( int tls_index, void * p ) noexcept;
}

} }
为了提高效率,read_void_ptrwrite_void_ptr 应定义为 inline

BOOST_LEAF_USE_TLS_ARRAY 下,识别以下附加配置宏

  • BOOST_LEAF_CFG_TLS_ARRAY_START_INDEX 指定 LEAF 可用的起始 TLS 数组索引(如果宏保持未定义,则 LEAF 将其定义为 0)。

  • 可以定义 BOOST_LEAF_CFG_TLS_ARRAY_SIZE 以指定 TLS 数组的大小。在这种情况下,TLS 索引在传递给 read_void_ptr / write_void_ptr 之前,会通过 BOOST_LEAF_ASSERT 进行验证。

  • 可以定义 BOOST_LEAF_CFG_TLS_INDEX_TYPE 以指定用于存储分配的 TLS 索引的整数类型(如果宏保持未定义,则 LEAF 将其定义为 unsigned char)。

报告程序不用于处理故障的类型的错误对象不会消耗 TLS 指针。LEAF 所需的 TLS 指针数组的最小大小是用作错误处理程序参数(在整个程序中)的不同类型的总数加一。
请注意 read_void_ptr/write_void_ptr 访问超出线程本地指针数组静态边界的线程本地指针;这可能会导致未定义的行为。

嵌入式平台

定义 BOOST_LEAF_EMBEDDED 等效于以下内容

#ifndef BOOST_LEAF_CFG_DIAGNOSTICS
#   define BOOST_LEAF_CFG_DIAGNOSTICS 0
#endif

#ifndef BOOST_LEAF_CFG_STD_SYSTEM_ERROR
#   define BOOST_LEAF_CFG_STD_SYSTEM_ERROR 0
#endif

#ifndef BOOST_LEAF_CFG_STD_STRING
#   define BOOST_LEAF_CFG_STD_STRING 0
#endif

#ifndef BOOST_LEAF_CFG_CAPTURE
#   define BOOST_LEAF_CFG_CAPTURE 0
#endif

LEAF 开箱即用地支持 FreeRTOS,请定义 BOOST_LEAF_TLS_FREERTOS(在这种情况下,如果尚未定义,LEAF 会自动定义 BOOST_LEAF_EMBEDDED)。

对于其他嵌入式平台,请定义 BOOST_LEAF_USE_TLS_ARRAY,请参阅 配置 TLS 访问

如果您的程序根本不使用并发,只需定义 BOOST_LEAF_NO_THREADS,这不需要任何 TLS 支持(但 不是 线程安全的)。

与流行的看法相反,异常处理在嵌入式平台上运行良好。在 此演讲 中,Khalil Estell 演示了使用异常处理错误可以显着减少固件代码大小(当然,LEAF 可以在有或没有异常处理的情况下工作)。

可移植性

源代码与 C++11 或更高版本兼容。

LEAF 使用线程本地存储(仅用于指针)。默认情况下,这是通过 C++11 thread_local 存储类说明符实现的,但该库可以轻松配置为使用任何平台特定的 TLS API(它附带了对 FreeRTOS 的内置支持)。请参阅 配置

运行单元测试

单元测试可以使用 Meson Build 或 Boost Build 运行。要运行单元测试

Meson 构建

将 LEAF 克隆到任何本地目录并执行

cd leaf
meson bld/debug
cd bld/debug
meson test

有关可用的构建选项,请参阅根目录中的 meson_options.txt

Boost 构建

假设当前工作目录是 <boostroot>/libs/leaf

../../b2 test

设计原理

定义

携带有关错误条件的信息的对象称为错误对象。例如,类型为 std::error_code 的对象是错误对象。

以下推理独立于用于传输错误对象的机制,无论是异常处理还是其他任何机制。
定义

根据函数与错误对象的交互方式,可以将函数分类如下

  • 错误发起:通过创建新的错误对象来发起错误条件的函数。

  • 错误中立:将较低级别函数调用的错误对象转发给调用方的函数。

  • 错误处理:处理已接收的错误对象,恢复正常程序运行的函数。

一个关键的观察是,错误发起 函数通常是低级别函数,它们缺乏任何上下文,无法确定,更不用说指示,针对它们可能发起的错误采取正确的程序行为。在某些程序中(正确地)导致终止的错误条件可能在其他程序中(正确地)被忽略;而另一些程序可能会从中恢复并恢复正常运行。

同样的推理也适用于 错误中立 函数,但在这种情况下,还存在一个额外的问题,即它们需要传递的错误通常是由调用链中多个级别之外的函数发起的,这些函数通常是 — 并且应该被视为 — 实现细节。错误中立 函数不应与 错误发起 函数传递的错误对象类型耦合,原因与它不应与它们接口的任何其他方面耦合的原因相同。

最后,错误处理 函数,根据定义,具有处理至少某些(如果不是全部)故障所需的完整上下文。在它们的范围内,绝对必要的是,作者确切地知道较低级别函数必须传递哪些信息才能从每个错误条件中恢复。具体而言,所有这些必要信息都不能被视为实现细节;在这种情况下,在 错误中立 函数中要避免的耦合实际上是可取的。

我们现在准备定义我们的

设计目标
  • 错误发起 函数应能够传递 所有 可用于它们的、与正在报告的故障相关的信息。

  • 错误中立 函数不应与较低级别 错误发起 函数传递的错误类型耦合。它们应该能够使用可用于它们的其他相关信息来增强任何故障。

  • 错误处理 函数应能够访问 错误发起错误中立 函数传递的所有信息,这些信息是处理故障所需要的。

错误中立 函数不与通过它们的错误对象的静态类型耦合的设计目标似乎需要动态多态性和动态内存分配(Boost Exception 库以动态内存分配为代价满足此设计目标)。

事实证明,动态内存分配是不必要的,因为以下

事实
  • 错误处理 函数“知道”错误发起错误中立 函数 能够 传递的信息中,哪些是 实际需要的,以便处理特定程序中的故障。理想情况下,不应 使用 浪费资源来存储或传递当前不需要用于处理错误的信息,即使它与故障相关

例如,如果库函数能够传递错误代码,但程序不需要知道确切的错误代码,那么在库函数尝试传递它时,可以忽略该信息。另一方面,如果 错误处理 函数需要该信息,则存储它所需的内存可以静态地在其范围内保留。

LEAF 函数 try_handle_sometry_handle_alltry_catch 实现了这个想法。用户提供错误处理 lambda 函数,每个函数都接受从特定错误条件恢复所需的类型的参数。LEAF 只是提供了存储这些类型所需的空间(以 std::tuple 的形式,使用自动存储持续时间),直到它们被传递给合适的处理程序。

在错误处理函数的范围内保留此空间时,所需错误类型的 thread_local 指针将设置为指向其中的相应对象。稍后,想要传递给定类型 E 的错误对象的 错误发起错误中立 函数将使用相应的 thread_local 指针来检测当前是否为此类型提供了存储空间

  • 如果指针不为空,则表示存储空间可用,并且对象将移动到指向的存储空间中,恰好一次 — 无论在到达 错误处理 函数之前必须展开多少级别的函数调用。

  • 如果指针为空,则表示存储空间不可用,并且错误对象将被丢弃,因为在此程序中没有任何错误处理函数使用它 — 从而节省资源。

这几乎可以工作,但我们需要确保 错误处理 函数受到保护,免于访问响应先前故障而存储的陈旧错误对象,这将是一个严重的逻辑错误。为此,每次错误发生都会分配一个唯一的 error_id。存储在错误处理范围内的每个 E…​ 对象也被分配一个 error_id,将其永久地与特定的故障关联起来。

因此,要处理故障,我们只需将可用的错误对象(与其唯一的 error_id 相关联)与每个用户提供的错误处理函数所需的参数类型进行匹配。在 C++ 异常处理方面,就好像我们可以编写类似以下内容:

try
{
  auto r = process_file();

  //Success, use r:
  ....
}

catch(file_read_error &, e_file_name const & fn, e_errno const & err)
{
  std::cerr <<
    "Could not read " << fn << ", errno=" << err << std::endl;
}

catch(file_read_error &, e_errno const & err)
{
  std::cerr <<
    "File read error, errno=" << err << std::endl;
}

catch(file_read_error &)
{
  std::cerr << "File read error!" << std::endl;
}

当然,此语法无效,因此 LEAF 使用 lambda 函数来表达相同的想法

leaf::try_catch(

  []
  {
    auto r = process_file(); //Throws in case of failure, error objects stored inside the try_catch scope

    //Success, use r:
    ....
  }

  [](file_read_error &, e_file_name const & fn, e_errno const & err)
  {
    std::cerr <<
      "Could not read " << fn << ", errno=" << err << std::endl;
  },

  [](file_read_error &, e_errno const & err)
  {
    std::cerr <<
      "File read error, errno=" << err << std::endl;
  },

  [](file_read_error &)
  {
    std::cerr << "File read error!" << std::endl;
  } );

类似的语法也适用于没有异常处理的情况。以下是使用 result<T> 编写的相同代码片段

return leaf::try_handle_some(

  []() -> leaf::result<void>
  {
    BOOST_LEAF_AUTO(r, process_file()); //In case of errors, error objects are stored inside the try_handle_some scope

    //Success, use r:
    ....

    return { };
  }

  [](leaf::match<error_enum, file_read_error>, e_file_name const & fn, e_errno const & err)
  {
    std::cerr <<
      "Could not read " << fn << ", errno=" << err << std::endl;
  },

  [](leaf::match<error_enum, file_read_error>, e_errno const & err)
  {
    std::cerr <<
      "File read error, errno=" << err << std::endl;
  },

  [](leaf::match<error_enum, file_read_error>)
  {
    std::cerr << "File read error!" << std::endl;
  } );

局限性

使用动态链接时,要求错误类型以 default 可见性声明,例如

struct __attribute__ ((visibility ("default"))) my_error_info
{
    int value;
};

除了在 Windows 上,这都可以按预期工作,在 Windows 上,线程本地存储在各个二进制模块之间不共享。因此,要在 DLL 边界之间传输错误对象。

使用动态链接时,始终最好使用 C 定义模块接口(并在适当的情况下使用 C++ 实现它们)。

LEAF 的替代方案

下面我们提供了 Boost LEAF 与 Boost Exception 和 Boost Outcome 的比较。

与 Boost 异常的比较

虽然 LEAF 可以在不使用异常处理的情况下使用,但在通过抛出异常来传递错误用例中,它可以被视为 Boost Exception 的更好、更高效的替代方案。LEAF 比 Boost Exception 具有以下优势

  • LEAF 不会动态分配内存;

  • LEAF 不会浪费系统资源来传递特定错误处理函数未使用的错误对象;

  • LEAF 不会将错误对象存储在异常对象中,因此它能够增强外部库抛出的异常(Boost Exception 只能增强派生自 boost::exception 的类型的异常)。

下表概述了当使用 Boost Exception 的代码重构为使用 LEAF 时应考虑的两个库之间的差异。

可以使用 LEAF 错误处理接口访问 Boost Exception 错误信息。请参阅 Boost Exception 集成
表 1. 定义用于传输 T 类型值的自定义类型
Boost Exception LEAF
typedef error_info<struct my_info_,T> my_info;
struct my_info { T value; };
表 2. 在抛出点传递任意信息
Boost Exception LEAF
throw my_exception() <<
  my_info(x) <<
  my_info(y);
leaf::throw_exception( my_exception(),
  my_info{x},
  my_info{y} );
表 3. 在错误中立上下文中增强异常
Boost Exception LEAF
try
{
  f();
}
catch( boost::exception & e )
{
  e << my_info(x);
  throw;
}
auto load = leaf::on_error( my_info{x} );

f();
表 4. 在捕获点获取任意信息
Boost Exception LEAF
try
{
  f();
}
catch( my_exception & e )
{
  if( T * v = get_error_info<my_info>(e) )
  {
    //my_info is available in e.
  }
}
leaf::try_catch(
  []
  {
    f(); // throws
  }
  [](my_exception &, my_info const & x)
  {
    //my_info is available with
    //the caught exception.
  } );
表 5. 错误对象的传输
Boost Exception LEAF

所有提供的 boost::error_info 对象都动态分配并存储在异常对象的 boost::exception 子对象中。

用户定义的错误对象静态存储在 try_catch 的范围内,但仅当它们的类型需要用于处理错误时才存储;否则它们将被丢弃。

表 6. 错误对象跨线程边界的传输
Boost Exception LEAF

boost::exception_ptr 会自动捕获存储在 boost::exception 中的 boost::error_info 对象,并可以在线程边界之间传输它们。

跨线程边界传输错误对象需要使用 [capture]

表 7. 在自动生成的诊断信息消息中打印错误对象
Boost Exception LEAF

boost::error_info 类型可以通过提供 to_string 重载 通过为 std::ostream 重载 operator<< 来定义到 std::string 的转换。

LEAF 不使用 to_string。错误类型可以为 std::ostream 定义 operator<< 重载。

Boost Exception 存储所有提供的 boost::error_info 对象 — 而 LEAF 在不需要时会丢弃它们 — 这一事实会影响我们打印 leaf::<<diagnostic_info> 对象时获得的消息的完整性,与 boost::diagnostic_information 返回的字符串相比。

如果用户需要完整的诊断消息,解决方案是使用 leaf::diagnostic_details。在这种情况下,在 LEAF 丢弃未使用的错误对象之前,它们会被转换为字符串并打印。请注意,这会动态分配内存。


与 Boost Outcome 的比较

设计差异

与 LEAF 类似,Boost Outcome 库旨在在低延迟环境中使用。它提供了两个类模板,result<>outcome<>

  • result<T,EC,NVP> 可以用作可能失败的 noexcept 函数中的返回类型,其中 T 指定成功情况下的返回值类型,而 EC 是“错误代码”类型。从语义上讲,result<T,EC> 类似于 std::variant<T,EC>。当然,EC 默认为 std::error_code

  • outcome<T,EC,EP,NVP> 类似于 result<>,但在失败的情况下,除了“错误代码”类型 EC 之外,它还可以保存类型为 EP 的“指针”对象,默认为 std::exception_ptr

NVP 是一种策略类型,用于自定义当 result<>outcome<> 对象包含错误时 .value() 的行为。

其思想是使用 result<> 来传递可以用“错误代码”完全指定的故障,并使用 outcome<> 来传递需要附加信息的故障。

描述此设计的另一种方法是,当返回某些静态类型 EC 的错误对象就足够时,使用 result<>,而 outcome<> 还可以使用指针类型 EP 传输多态错误对象。

outcome<T> 的默认配置中,附加信息 — 或附加的多态对象 — 是由 std::exception_ptr 持有的异常对象。这针对的是当较低级别库函数抛出的异常需要通过一些非异常安全的中间上下文传输到能够处理它的更高级别上下文时的用例。LEAF 也直接支持此用例,请参阅 exception_to_result

类似的原因也推动了 LEAF 的设计。不同之处在于,虽然这两个库都认识到除了“错误代码”之外还需要传输“其他东西”,但 LEAF 为此问题提供了高效的解决方案,而 Outcome 将此负担转移给了用户。

leaf::result<> 模板删除了 ECEP,这使其与在失败情况下传输的错误对象的类型解耦。这使较低级别的函数可以自由地传递它们“知道”的关于故障的任何和所有信息:错误代码,甚至多个错误代码、文件名、URL、端口号等。同时,更高级别的错误处理函数控制特定客户端程序中需要哪些信息,哪些信息不需要。这是理想的,因为

  • 较低级别库函数的作者缺乏上下文来确定哪些既与错误相关 自然可用于它们的信息需要传递,以便特定客户端程序可以从该错误中恢复;

  • 更高级别错误处理函数的作者可以轻松且自信地做出此判断,并通过简单地编写不同的错误处理程序,自然地将其传达给 LEAF。LEAF 将传输所需的错误对象,同时丢弃处理程序不关心的对象,从而节省资源。

LEAF 示例包括对 Boost Outcome result<> 教程中程序的改编。您可以在 GitHub 上查看它
使用 LEAF 进行错误处理的程序不需要使用 leaf::result<T>;例如,可以将 outcome::result<T> 与 LEAF 一起使用。

互操作性问题

Boost Outcome 文档讨论了一个重要问题,即将多个库(每个库都使用自己的错误报告机制)整合在一起,并将它们纳入客户端程序中健壮的错误处理基础设施中。

建议用户尽可能在整个代码库中使用通用的错误处理系统,但由于这不切实际,result<>outcome<> 模板都可以携带用户定义的“payload”。

以下分析来自 Boost Outcome 文档

如果库 A 使用 result<T, libraryA::failure_info>,库 B 使用 result<T, libraryB::error_info>,依此类推,那么对于应用程序编写者来说就出现了一个问题,他们引入了这些第三方依赖项并将它们绑定到一个应用程序中。作为一般规则,每个第三方库作者都不会为未知的其他第三方库构建显式的互操作支持。因此,问题落到了应用程序编写者身上。

应用程序编写者有三种选择

  1. 在应用程序中,使用的 result 形式是 result<T, std::variant<E1, E2, …​>>,其中 E1, E2 … 是应用程序中使用的每个第三方库的失败类型。这样做的好处是完全保留了原始信息,但带来了一定的使用不便,并且可能导致高层和实现细节之间过度耦合。

  2. 可以在失败退出第三方库并进入应用程序的点,将第三方的失败类型翻译/映射到应用程序的失败类型。例如,可以使用 C 预处理器宏来包装应用程序对第三方 API 的每次调用。如果两个系统之间的映射不是一对一的,则此方法可能会丢失原始失败的详细信息,或者在某些情况下错误映射。

  3. 可以将第三方的失败类型类型擦除为某种应用程序失败类型,如果需要,以后可以重新构建。这是最干净的解决方案,耦合问题最少,并且没有错误映射的问题,但这几乎肯定需要使用 malloc,而前两种方法不需要。

上述分析(强调部分是添加的)清晰而精确,但 LEAF 和 Boost Outcome 以不同的方式解决了互操作性问题

  • Boost Outcome 设计断言,基于类型擦除的“最干净”解决方案是次优的(“几乎肯定需要使用 malloc”),并且提供了一个系统,用于将自定义转换器注入到 outcome::convert 命名空间中,用于在库特定的错误类型和程序范围的错误类型之间进行转换,即使这种方法“可能会丢失原始失败的详细信息”。

  • LEAF 设计断言,将错误中性函数的签名与它们需要转发给调用者的错误对象的静态类型耦合不具有可扩展性,而是直接将错误对象传输到错误处理作用域,在这些作用域中它们被静态存储,从而有效地实现了上面概述的第三种选择(无需使用 malloc)。

此外,考虑到 Outcome 的目标是希望成为唯一所有库都将使用的错误处理 API,并且理论上每个人都将从统一和标准化中受益。但现实是,这只是一厢情愿的想法。事实上,这种现实反映在 outcome::result<> 的设计中,反映在其缺乏对使用 std::error_code 的承诺,以实现其预期目的:成为传输错误代码的标准类型。事实是,std::error_code 成为了程序员需要理解和支持的又一种错误代码类型。

相比之下,LEAF 的设计承认 C++ 程序员甚至在字符串是什么这个问题上都没有达成一致。如果您的项目使用 10 个不同的库,这可能意味着有 15 种不同的报告错误的方式,有时会跨越不合作的接口(例如 C API)。LEAF 帮助您完成工作。

致谢

特别感谢 Peter Dimov 和 Sorin Fetche。

Ivo Belchev、Sean Palmer、Jason King、Vinnie Falco、Glen Fernandes、Augustín Bergé — 感谢宝贵的反馈。

文档由 Asciidoctor 使用 这些自定义设置 渲染。