Boost C++ 库

...one of the most highly regarded and expertly designed C++ library projects in the world. Herb Sutter and Andrei Alexandrescu, C++ Coding Standards

LEAF - Boost C++ 函数库

摘要

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

  • 便携式单头文件格式,无依赖。

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

  • 无动态内存分配,即使是包含大型有效载荷。

  • “快乐路径”和“悲伤路径”上均具有确定性的无偏效率。

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

  • 可以使用或不使用异常处理。

教程 | 概要 | 白皮书

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

支持

分发

LEAF 在 Boost Software License, Version 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 将正确分派错误处理器,无论故障是通过 leaf::result 还是通过异常传递。

当然,如果我们只使用异常处理,我们根本不需要 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++ 程序需要通过各种错误代码、result 类型和异常来处理通过多层 API 传递的错误。

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。我们只需要让 LEAF 知道,通过专门化 is_result_type 模板

namespace boost { namespace leaf {

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

} }

有了这个,f 的工作方式与以前一样,尽管 lib3::result 无法传输 lib1 错误或 lib2 错误

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 返回的对象隐式转换为 std::error_code,使用 LEAF 特定的 error_category,这使得 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,然后该错误 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,它接受输入文件名和输出文件名作为参数,检测到失败,它可以使用 new_error 来通信错误码 ec 以及两个相关文件名

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 传递一个未被错误处理器使用的错误对象,它将被丢弃。如果我们传递一个返回错误对象的函数而不是错误对象,该函数将仅在需要其返回的对象时才会被调用,也就是说,如果它不会被丢弃。这在生成错误对象相对昂贵时很有用

    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 失败了;compute_info 只有在调用堆栈中存在接受 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 添加到 e_relevant_file_names 对象,并与 r 中通信的 error_id 相关联。但请注意,只有当调用堆栈中有接受 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_castEx 类型之一。

  • 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 枚举中很容易;但现在我们的输入错误处理程序将无法识别这种新的输入错误——这就产生了一个 bug。

使用异常是一种改进,因为异常类型可以组织成一个层次结构,以便对失败进行分类。

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 的函数注册为一个 C 回调,以便 Lua 程序可以调用它。

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 执行它。

接下来,我们定义用于传递 do_work 故障的 enum

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 结果。

最后,这是执行 call_luamain 函数,每次都会处理任何故障。

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 的输出告诉我们,我们得到一个值为 1write_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 的错误类型,用户仍可能希望重载封闭 structoperator<<,例如:

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
};

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

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 创建一个它实际用来与 ec 比较的 std::error_code 对象。

到目前为止一切顺利,但请记住,标准库还定义了另一种类型,即 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_code 对象的 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 Exception 集成

除了 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 访问)才会被选中。

也支持使用 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

参考:函数

每个 Reference 部分的内容按字母顺序组织。

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 返回类型 Texception_to_result 返回 leaf::result<T>

效果
  1. 捕获所有异常,然后将 std::current_exception 捕获到 std::exception_ptr 对象中,该对象会被 加载 到返回的 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... 类型必须是无异常移动的。

效果

如同

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... 类型必须是无异常移动的。

效果

所有 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 如果 exception 只带一个 error_id 对象调用,则抛出的异常类型是不确定的,它公开派生自 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 我们已传递 E1E3 错误对象……
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 是某个对 is_result_type 为 true 的类型 Result<T>,则 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 函数返回的类型 R 必须是 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_blockh…​ 中的任何一个抛出异常。
处理程序选择过程

如果 try_handle_some 能够使用 ctx 中当前可用的、与 r.error() 返回的错误 ID 关联的错误对象来生成参数,则处理程序 h 就适合处理 r 报告的失败。一旦确定无法生成参数值,当前处理程序将被丢弃,并且选择过程继续处理下一个处理程序(如果存在)。

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

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

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

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

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

    • 否则,丢弃该处理程序。

    示例
    ....
    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 总是能够生成它:首先尝试按引用方式生成;如果失败,则不丢弃处理程序,而是用 0 初始化 ai

    示例
    ....
    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 此处理程序可以被选来处理任何错误,因为它以 const *(且仅此一项)的形式接收 e_file_name
    2 如果当前错误可用 e_file_name,则打印它。
  • 如果 ai 的类型是谓词类型 Pred(对于该类型 is_predicate<Pred>::valuetrue),则 E 推导为 typename Pred::error_type,然后

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

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

    • 为了调用处理程序,Pred 参数 aiPred{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 返回。

参考:类型

每个 Reference 部分的内容按字母顺序组织。

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 对象本身来完成。

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

要能够将错误对象加载到 context 对象中,它必须被激活。激活 context 对象 ctx 会将其绑定到调用线程,将存储的 E…​ 类型的线程局部指针设置为指向 ctx 内相应的存储。在任何给定线程中可能存在(甚至很可能)一个以上的激活 context。在这种情况下,激活/去激活必须按 LIFO 顺序进行。因此,最好使用 context_activator,它依赖 RAII 来激活和去激活 context

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

虽然错误处理通常使用 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 对象会导致未定义行为。

activate

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

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

} }
要求

!is_active().

效果

*this 与调用线程关联。

确保

is_active().

当一个 context 与一个线程关联时,线程局部指针被设置为指向其存储中的每个 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().

当一个 context 被去激活时,当前指向其每个单独错误对象存储的线程局部指针将被恢复到调用 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 的错误对象都会被移动到另一个提供类型 E 存储的激活的 context 对象中(如果存在),否则会被丢弃。目标对象不会被覆盖,除非它们与指定的 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,则构造函数和析构函数没有效果。否则

  • 构造函数将 ctx 的引用存储在 *this 中并调用 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 使用一个未指定的 std::error_category,LEAF 能够识别它。这允许 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

  • 否则,用 new_error 返回的值初始化 *this,同时将 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 来标识它携带的是 error 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... 类型必须是无异常移动的。

效果
  • 如果 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 为 true,并不意味着 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;
  };

} }

此类旨在获取一个 error_id 以将错误对象与之关联,当通过不合作的 API(不使用 LEAF 报告错误,因此在错误时不会返回 error_id)将 LEAF 传播的故障进行增强时。

此类常见的用法如下:

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

result 对象使用 std::error_code 对象初始化时,它用于初始化 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
  {
  };

} }

参考:谓词

每个 Reference 部分的内容按字母顺序组织。

谓词是一种特殊的错误处理程序参数,它使 处理程序选择过程 能够考虑可用错误对象的,而不仅仅是它们的类型;请参阅 使用谓词处理错误

提供以下谓词

此外,任何用户定义的类型 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 参数将使用 Pred{e} 初始化。

谓词在调用错误处理程序之前进行评估,因此它们不能访问动态状态(当然,错误处理程序本身可以通过 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).

特别是,将函数指针 leaf::category<Enum> 传递给任何 Vi 是有效的,其中

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,请参阅示例 4 获取 C++11 兼容的解决方法)。
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 的对象,该对象的 std::error_categoryenum_a 相同,或者等于 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:处理严重性大于指定阈值的失败(至少需要 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,则选择该处理程序。

参考:特征

每个 Reference 部分的内容按字母顺序组织。

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

参考:宏

每个 Reference 部分的内容按字母顺序组织。

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()

BOOST_LEAF_ASSIGN 在调用返回 result<T>(非 result<void>)的函数时很有用,如果期望的行为是将任何错误原样转发给调用者。

在成功的情况下,类型为 T 的结果 value() 被赋值给指定的变量 v,该变量必须在调用 BOOST_LEAF_ASSIGN 之前声明。但是,可以通过传递 v 的类型和名称来使用 BOOST_LEAF_ASSIGN 来声明一个新变量,例如 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)

BOOST_LEAF_AUTO 在调用返回 result<T>(非 result<void>)的函数时很有用,如果期望的行为是将任何错误原样转发给调用者。

示例
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

BOOST_LEAF_CHECK 在调用返回 result<void> 的函数时很有用,如果期望的行为是将任何错误原样转发给调用者。

示例
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__ 下默认为 1),则 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 会 stub 掉 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:此宏控制是否将 BOOST_LEAF_CHECK 定义为 GNU C 语句表达式,这使其可以像某些语言中的问号运算符一样用于检查错误(参见 检查错误)。默认情况下,在 __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 关键字实现的,但为了支持 嵌入式平台,可以通过定义 BOOST_LEAF_USE_TLS_ARRAY 来配置 LEAF 使用线程局部指针数组,在这种情况下,用户需要定义以下两个函数来实现所需的 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 索引会通过 BOOST_LEAF_ASSERT 进行验证,然后传递给 read_void_ptr / write_void_ptr

  • 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 指针来检测当前是否有可用的该类型的存储空间。

  • 如果指针不为 null,则存储可用,对象会被移动到指向的存储中,最多一次——无论必须展开多少层函数调用才能到达错误处理函数。

  • 如果指针为 null,则存储不可用,错误对象将被丢弃,因为在该程序中没有错误处理函数使用它——节省了资源。

这几乎可行,除了我们需要确保错误处理函数免受访问因先前失败而存储的陈旧错误对象的侵害,这将是一个严重的逻辑错误。为此,每次错误发生都被分配一个唯一的 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 上,线程局部存储在各个二进制模块之间不共享。因此,要在 DLL 边界上传输错误对象。

使用动态链接时,最好始终根据 C 定义模块接口(并在适当的情况下用 C++ 实现它们)。

LEAF 的替代方案

下面我们提供 Boost LEAF 与 Boost Exception 和 Boost Outcome 的比较。

与 Boost Exception 的比较

虽然 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 重载 **或** 通过重载 operator<< for std::ostream 来定义转换为 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 是一种策略类型,用于自定义 .value()result<>outcome<> 对象包含错误时的行为。

其思想是使用 result<> 来传达可以通过“错误码”完全指定的失败,并使用 outcome<> 来传达需要附加信息的失败。

另一种描述此设计的方式是,result<> 用于当返回某种静态类型 EC 的错误对象已足够时,而 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<> 模板都可以携带用户定义的“有效负载”。

以下分析来自 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 使用 这些自定义设置 渲染。