摘要
分发
教程
什么是失败?它仅仅是函数无法返回有效结果,而是生成一个描述失败原因的错误对象。
一种典型的设计是返回变体类型,例如 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::e1
或err1::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_error
或 parse_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_line
。on_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_some
和 try_handle_all
都会捕获异常,并且能够将它们传递给任何兼容的错误处理程序
leaf::result<void> r = leaf::try_handle_some(
[]() -> leaf::result<void>
{
BOOST_LEAF_CHECK( process_file(f) );
},
[]( std::bad_alloc const & )
{
std::cerr << "Out of memory!" << std::endl;
},
[]( parse_error e, e_line l )
{
std::cerr << "Parse error at line " << l.value << std::endl;
},
[]( io_error e, e_line const * l )
{
std::cerr << "Parse error";
if( l )
std::cerr << " at line " << l.value;
std::cerr << std::endl;
} );
上面,我们只是添加了一个接受 std::bad_alloc
的错误处理程序,一切都“按预期工作”:无论故障是通过 leaf::result
还是通过异常传递,LEAF 都会正确调度错误处理程序。
当然,如果我们专门使用异常处理,则根本不需要 leaf::result
。在这种情况下,我们使用 leaf::try_catch
leaf::try_catch(
[]
{
process_file(f);
},
[]( std::bad_alloc const & )
{
std::cerr << "Out of memory!" << std::endl;
},
[]( parse_error e, e_line l )
{
std::cerr << "Parse error at line " << l.value << std::endl;
},
[]( io_error e, e_line const * l )
{
std::cerr << "Parse error";
if( l )
std::cerr << " at line " << l.value;
std::cerr << std::endl;
} );
我们不必更改错误处理程序!但这如何工作?process_file
抛出什么样的异常?
LEAF 启用了一种新颖的异常处理技术,该技术不需要异常类型层次结构来对故障进行分类,也不在异常对象中携带数据。回想一下,当故障通过 leaf::result
传递时,我们在 return
语句中调用 leaf::new_error
,传递任意数量的错误对象,这些对象直接发送到正确的错误处理范围
enum class err1 { e1, e2, e3 };
enum class err2 { e1, e2 };
....
leaf::result<T> f()
{
....
if( error_detected )
return leaf::new_error(err1::e1, err2::e2);
// Produce and return a T.
}
当使用异常处理时,这变为
enum class err1 { e1, e2, e3 };
enum class err2 { e1, e2 };
T f()
{
if( error_detected )
leaf::throw_exception(err1::e1, err2::e2);
// Produce and return a T.
}
leaf::throw_exception
函数像 leaf::new_error
一样处理传递的错误对象,然后抛出一个类型派生自 std::exception
的对象。使用此技术,异常类型并不重要:leaf::try_catch
捕获所有异常,然后执行通常的 LEAF 错误处理程序选择例程。
相反,如果我们想使用抛出不同类型来指示不同故障的常用约定,我们只需将异常对象(即类型派生自 std::exception
的对象)作为第一个参数传递给 leaf::throw_exception
leaf::throw_exception(std::runtime_error("Error!"), err1::e1, err2::e2);
在这种情况下,抛出的异常对象将是类型派生自 std::runtime_error
,而不是 std::exception
。
最后,leaf::on_error
也“可以正常工作”。这是我们的 process_file
函数,经过重写以使用异常而不是返回 leaf::result
(请参阅 增强错误)
int parse_line( FILE * f ); // Throws
struct e_line { int value; };
void process_file( FILE * f )
{
for( int current_line = 1; current_line != 10; ++current_line )
{
auto load = leaf::on_error( e_line {current_line} );
int v = parse_line(f);
// use v
}
}
使用外部 result
类型
静态类型检查在任何重要的项目中的错误处理互操作性方面都会造成困难。使用异常处理在某种程度上缓解了这个问题,因为在这种情况下,错误类型不会被烧录到函数签名中,因此错误很容易穿透多层 API;但这通常对 C++ 没有帮助,因为社区在异常处理问题上存在分歧。尽管存在争议,但现实情况是 C++ 程序需要通过多层 API 处理通过大量错误代码、result
类型和异常传递的错误。
LEAF 使应用程序开发人员能够从每个单独库的 result
类型中抖出错误对象,并将它们逐字发送到错误处理范围。这是一个示例
lib1::result<int, lib1::error_code> foo();
lib2::result<int, lib2::error_code> bar();
int g( int a, int b );
leaf::result<int> f()
{
auto a = foo();
if( !a )
return leaf::new_error( a.error() );
auto b = bar();
if( !b )
return leaf::new_error( b.error() );
return g( a.value(), b.value() );
}
稍后我们只需调用 leaf::try_handle_some
,并为每种类型传递一个错误处理程序
leaf::result<int> r = leaf::try_handle_some(
[]() -> leaf::result<int>
{
return f();
},
[]( lib1::error_code ec ) -> leaf::result<int>
{
// Handle lib1::error_code
},
[]( lib2::error_code ec ) -> leaf::result<int>
{
// Handle lib2::error_code
} );
}
一个可能的复杂情况是,我们可能无法选择从 f
返回 leaf::result<int>
:第三方 API 可能会对其施加特定的签名,强制其返回库特定的 result
类型。当 f
旨在用作回调时,情况就是如此
void register_callback( std::function<lib3::result<int>()> const & callback );
在这种情况下我们可以使用 LEAF 吗?实际上,只要 lib3::result
能够传递 std::error_code
,我们就可以使用。我们只需要通过专门化 is_result_type
模板来告知 LEAF
namespace boost { namespace leaf {
template <class T>
struct is_result_type<lib3::result<T>>: std::true_type;
} }
有了这个,即使 lib3::result
无法传输 lib1
错误或 lib2
错误,f
也能像以前一样工作
lib1::result<int, lib1::error_type> foo();
lib2::result<int, lib2::error_type> bar();
int g( int a, int b );
lib3::result<int> f() // Note: return type is not leaf::result<int>
{
auto a = foo();
if( !a )
return leaf::new_error( a.error() );
auto b = bar();
if( !b )
return leaf::new_error( b.error() );
return g( a.value(), b.value() );
}
leaf::new_error
返回的对象使用 LEAF 特定的 error_category
隐式转换为 std::error_code
,这使得 lib3::result
与 leaf::try_handle_some
(以及 leaf::try_handle_all
)兼容
lib3::result<int> r = leaf::try_handle_some(
[]() -> lib3::result<int>
{
return f();
},
[]( lib1::error_code ec ) -> lib3::result<int>
{
// Handle lib1::error_code
},
[]( lib2::error_code ec ) -> lib3::result<int>
{
// Handle lib2::error_code
} );
}
互操作性
理想情况下,当检测到错误时,使用 LEAF 的程序始终会调用 new_error
,确保遇到的每个故障都绝对分配一个唯一的 error_id
,然后通过异常或 result<T>
对象可靠地传递到适当的错误处理范围。
唉,这并非总是可能的。
例如,错误可能需要通过不合作的第三方接口进行传递。为了方便这种传输,错误 ID 可以编码在 std::error_code
中。只要第三方接口能够传输 std::error_code
,它就可以与 LEAF 兼容。
此外,有时需要通过甚至不使用 std::error_code
的接口来传递错误。一个例子是当外部底层库抛出异常时,该异常不太可能携带 error_id
。
为了支持这种棘手的用例,LEAF 提供了函数 current_error
,该函数返回(从此线程)最近一次调用 new_error
返回的错误 ID。解决问题的一种可能方法是使用以下逻辑(由 error_monitor
类型实现)
-
在调用不合作的 API 之前,调用
current_error
并缓存返回的值。 -
调用 API,然后再次调用
current_error
-
如果这返回的值与之前相同,则将错误对象传递给
new_error
,以将它们与新的error_id
关联起来; -
否则,将错误对象与第二次调用
current_error
返回的error_id
值关联起来。
-
请注意,如果上述逻辑是嵌套的(例如,一个函数调用另一个函数),则 new_error
将仅由最内层的函数调用,因为该调用保证所有调用函数都将命中 else
分支。
有关详细教程,请参阅 使用 error_monitor
从 C 回调报告任意错误。
加载错误对象
回想一下,传递给 LEAF 的错误对象存储在堆栈上,本地于用于处理错误的 try_handle_same
、try_handle_all
或 try_catch
函数。加载错误对象意味着将其移动到此类存储中(如果可用)。
各种 LEAF 函数都采用要加载的错误对象列表。例如,如果一个函数 copy_file
(它将输入文件名和输出文件名作为其参数)检测到故障,则它可以传递错误代码 ec
,以及使用 new_error
的两个相关文件名
return leaf::new_error(ec, e_input_name{n1}, e_output_name{n2});
或者,可以使用已经传递错误的 result<T>
加载错误对象。这样,它们就与该错误而不是新错误关联起来
leaf::result<int> f() noexcept;
leaf::result<void> g( char const * fn ) noexcept
{
if( leaf::result<int> r = f() )
{ (1)
....;
return { };
}
else
{
return r.load( e_file_name{fn} ); (2)
}
}
1 | 成功!使用 r.value() 。 |
2 | f() 失败;这里我们将额外的 e_file_name 与错误关联起来。但是,只有当通往 g 的调用堆栈中存在接受 e_file_name 参数的错误处理程序时,才会发生这种关联。否则,传递给 load 的对象将被丢弃。换句话说,只有当程序实际使用它们来处理错误时,才会加载传递的对象。 |
除了错误对象,load
还可以接受函数参数
-
如果我们传递一个不带参数的函数,它将被调用,并且返回的错误对象将被加载。
请考虑,如果我们传递给
load
一个错误处理程序未使用的错误对象,它将被丢弃。如果我们将返回错误对象的函数传递给load
而不是错误对象,则只有在需要它返回的对象时,即如果不会被丢弃时,才会调用该函数。当错误对象的生成成本相对较高时,这很有用struct info { .... }; info compute_info() noexcept; leaf::result<void> operation( char const * file_name ) noexcept { if( leaf::result<int> r = try_something() ) { (1) .... return { }; } else { return r.load( (2) [&] { return compute_info(); } ); } }
1 成功!使用 r.value()
。2 try_something
失败;只有当调用堆栈中存在接受info
参数的错误处理程序时,才会调用compute_info
。 -
如果我们传递一个函数,该函数接受类型为
E &
的单个参数,则 LEAF 使用当前加载在与错误关联的活动context
中的类型为E
的对象调用该函数。如果没有此类对象可用,则将默认初始化一个新对象,然后将其传递给函数。例如,如果涉及许多不同文件的操作失败,则程序可以提供用于在
e_relevant_file_names
对象中收集所有相关文件名的功能struct e_relevant_file_names { std::vector<std::string> value; }; leaf::result<void> operation( char const * file_name ) noexcept { if( leaf::result<int> r = try_something() ) { (1) .... return { }; } else { return r.load( (2) [&](e_relevant_file_names & e) { e.value.push_back(file_name); } ); } }
1 成功!使用 r.value()
。2 try_something
失败 — 将file_name
添加到与r
中传递的error_id
关联的e_relevant_file_names
对象。但是,请注意,只有当调用堆栈中存在接受e_relevant_file_names
对象的错误处理程序时,才会调用传递的函数。
使用 on_error
错误报告函数通常无法提供合适的错误处理函数为了从故障中恢复而需要的所有数据。例如,报告 FILE
故障的函数可能无法访问文件名,但错误处理函数需要该文件名才能打印有用的错误消息。
文件名通常在通往失败的 FILE
操作的调用堆栈中很容易获得。在下面,虽然 parse_info
无法报告文件名,但 parse_file
可以并且确实可以
leaf::result<info> parse_info( FILE * f ) noexcept; (1)
leaf::result<info> parse_file( char const * file_name ) noexcept
{
auto load = leaf::on_error(leaf::e_file_name{file_name}); (2)
if( FILE * f = fopen(file_name,"r") )
{
auto r = parse_info(f);
fclose(f);
return r;
}
else
return leaf::new_error( error_enum::file_open_error );
}
1 | parse_info 使用 leaf::result 传递错误。 |
2 | on_error 确保文件名包含在 parse_file 报告的任何错误中。当 load 对象过期时,如果正在报告错误,则传递的 e_file_name 值将自动与之关联。 |
on_error — 像 new_error 一样 — 可以传递任意数量的参数。 |
当我们调用 on_error
时,我们可以传递三种参数
-
实际错误对象(如上面的示例);
-
不带参数并返回错误对象的函数;
-
通过可变引用获取单个错误对象的函数。
例如,如果我们想使用 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
与它们中的任何一个都不相等,则会删除错误处理程序。
特别是,match
与 std::error_code
非常有效。以下处理程序旨在处理 ENOENT
错误
[]( leaf::match<std::error_code, std::errc::no_such_file_or_directory> )
{
}
但是,这需要 C++17 或更高版本。LEAF 提供了以下与 C++11 兼容的解决方法
[]( leaf::match<leaf::condition<std::errc>, std::errc::no_such_file_or_directory> )
{
}
也可以根据 std::error_category
选择处理程序。以下处理程序将匹配 std::generic_category
的任何 std::error_code
(需要 C++17 或更高版本)
[]( std::error_code, leaf::category<std::errc>> )
{
}
有关更多示例,请参阅 match 。 |
以下谓词可用
-
match
:如上所述。 -
match_value
:其中match<E, V…>
将类型为E
的对象e
与值V…
进行比较,match_value<E, V…>
将e.value
与值V…
进行比较。 -
match_member
:类似于match_value
,但采用指向要比较的数据成员的指针;也就是说,match_member<&E::value, V…>
等效于match_value<E, V…>
。但是,请注意,match_member
需要 C++17 或更高版本,而match_value
则不需要。 -
:类似于catch_
<Ex…>match
,但检查捕获的std::exception
对象是否可以dynamic_cast
到任何Ex
类型。 -
if_not
是一个特殊的谓词,它接受任何其他谓词Pred
,并要求类型为E
的错误对象可用,并且Pred
的计算结果为false
。例如,if_not<match<E, V…>>
要求类型为E
的对象e
可用,并且它与任何指定的V…
都不相等。
谓词系统易于扩展,请参阅 谓词。
重用通用错误处理程序
考虑以下代码段
leaf::try_handle_all(
[&]
{
return f(); // returns leaf::result<T>
},
[](my_error_enum x)
{
...
},
[](read_file_error_enum y, e_file_name const & fn)
{
...
},
[]
{
...
});
如果我们需要尝试一组不同的操作,但仍使用相同的处理程序,我们可以重复相同的操作,并将不同的函数作为 try_handle_all
的 TryBlock
传递
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_error
、read_error
或 eof_error
之一,则将调用它。简而言之,其思想是处理任何输入错误。
但是,如果稍后我们添加了对检测和报告新型输入错误(例如 permissions_error
)的支持怎么办?很容易将其添加到我们的 error_code
枚举中;但是现在我们的输入错误处理程序将无法识别这个新的输入错误 — 并且我们有一个错误。
使用异常是一种改进,因为异常类型可以组织在层次结构中,以便对故障进行分类
struct input_error: std::exception { };
struct read_error: input_error { };
struct size_error: input_error { };
struct eof_error: input_error { };
就 LEAF 而言,我们的输入错误异常处理程序现在看起来像这样
[](input_error &, leaf::e_file_name const & fn)
{
std::cerr << "Failed to access " << fn.value << std::endl;
},
这是面向未来的,但仍然不理想,因为在抛出异常对象后无法细化故障的分类。
LEAF 支持一种新颖的错误处理风格,其中故障的分类不使用错误代码值或异常类型层次结构。代替我们的 error_code
枚举,我们可以定义
....
struct input_error { };
struct read_error { };
struct size_error { };
struct eof_error { };
....
有了这个,我们可以定义一个函数 file_read
leaf::result<void> file_read( FILE & f, void * buf, int size )
{
int n = fread(buf, 1, size, &f);
if( ferror(&f) )
return leaf::new_error(input_error{}, read_error{}, leaf::e_errno{errno}); (1)
if( n!=size )
return leaf::new_error(input_error{}, eof_error{}); (2)
return { };
}
1 | 此错误分类为 input_error 和 read_error 。 |
2 | 此错误分类为 input_error 和 eof_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_a
和 error_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_a
和 error_b
对象是否作为异常抛出,它都能正常工作。
leaf::try_handle_all(
[]() -> leaf::result<void>
{
BOOST_LEAF_CHECK(print_answer());
return { };
},
[](error_a const & e)
{
std::cerr << "Error A!" << std::endl;
},
[](error_b const & e)
{
std::cerr << "Error B!" << std::endl;
},
[]
{
std::cerr << "Unknown error!" << std::endl;
} );
说明此技术的完整程序可在此处获取:here。 |
使用 error_monitor
从 C 回调报告任意错误
传达与 C 回调中检测到的失败相关的信息是棘手的,因为 C 回调被限制为特定的函数签名,该签名可能不使用 C++ 类型。
LEAF 使这变得容易。作为一个例子,我们将编写一个程序,该程序使用 Lua 并从注册为 C 回调的 C++ 函数报告失败,该回调从 Lua 程序调用。失败将从 C++ 传播,通过 Lua 解释器(用 C 编写),再回到调用它的 C++ 函数。
旨在从 Lua 程序调用的 C/C++ 函数必须使用以下签名
int do_work( lua_State * L ) ;
参数在 Lua 堆栈(可以通过 L
访问)上传递。结果也推送到 Lua 堆栈上。
首先,让我们初始化 Lua 解释器并注册一个名为 do_work
的函数,作为可供 Lua 程序调用的 C 回调
std::shared_ptr<lua_State> init_lua_state() noexcept
{
std::shared_ptr<lua_State> L(lua_open(), &lua_close); (1)
lua_register(&*L, "do_work", &do_work); (2)
luaL_dostring(&*L, "\ (3)
\n function call_do_work()\
\n return do_work()\
\n end");
return L;
}
1 | 创建一个新的 lua_State 。我们将使用 std::shared_ptr 进行自动清理。 |
2 | 将 do_work C++ 函数注册为 C 回调,全局名称为 do_work 。这样,从 Lua 程序到 do_work 的调用将落入 do_work C++ 函数中。 |
3 | 将一些 Lua 代码作为 C 字符串字面量传递给 Lua。这将创建一个名为 call_do_work 的全局 Lua 函数,我们稍后将要求 Lua 执行它。 |
接下来,让我们定义我们的 enum
,用于传达 do_work
失败
enum do_work_error_code
{
ec1=1,
ec2
};
我们现在准备好定义 do_work
回调函数了
int do_work( lua_State * L ) noexcept
{
bool success = rand() % 2; (1)
if( success )
{
lua_pushnumber(L, 42); (2)
return 1;
}
else
{
(void) leaf::new_error(ec1); (3)
return luaL_error(L, "do_work_error"); (4)
}
}
1 | “有时” do_work 会失败。 |
2 | 如果成功,将结果推送到 Lua 堆栈上,返回到 Lua。 |
3 | 生成一个新的 error_id 并将其与 do_work_error_code 关联。通常,我们会在 leaf::result<T> 中返回它,但 do_work 函数签名(Lua 要求)不允许这样做。 |
4 | 告诉 Lua 解释器中止 Lua 程序。 |
现在我们将编写调用 Lua 解释器来执行 Lua 函数 call_do_work
的函数,该函数反过来调用 do_work
。我们将返回
,以便我们的调用者可以在成功时获得答案,或者获得错误result
<int>
leaf::result<int> call_lua( lua_State * L )
{
lua_getfield(L, LUA_GLOBALSINDEX, "call_do_work");
error_monitor cur_err;
if( int err = lua_pcall(L, 0, 1, 0) ) (1)
{
auto load = leaf::on_error(e_lua_error_message{lua_tostring(L,1)}); (2)
lua_pop(L,1);
return cur_err.assigned_error_id().load(e_lua_pcall_error{err}); (3)
}
else
{
int answer = lua_tonumber(L, -1); (4)
lua_pop(L, 1);
return answer;
}
}
1 | 要求 Lua 解释器调用全局 Lua 函数 call_do_work 。 |
2 | on_error 像往常一样工作。 |
3 | load 将使用在我们的 Lua 回调中生成的 error_id 。这与 on_error 使用的 error_id 相同。 |
4 | 成功!只需返回 int 答案。 |
最后,这是 main
函数,它练习 call_lua
,每次都处理任何失败
int main() noexcept
{
std::shared_ptr<lua_State> L=init_lua_state();
for( int i=0; i!=10; ++i )
{
leaf::try_handle_all(
[&]() -> leaf::result<void>
{
BOOST_LEAF_AUTO(answer, call_lua(&*L));
std::cout << "do_work succeeded, answer=" << answer << '\n'; (1)
return { };
},
[](do_work_error_code e) (2)
{
std::cout << "Got do_work_error_code = " << e << "!\n";
},
[](e_lua_pcall_error const & err, e_lua_error_message const & msg) (3)
{
std::cout << "Got e_lua_pcall_error, Lua error code = " << err.value << ", " << msg.value << "\n";
},
[](leaf::error_info const & unmatched)
{
std::cerr << "Unknown failure detected\n" << unmatched;
} );
}
1 | 如果对 call_lua 的调用成功,则只需打印答案。 |
2 | 处理 do_work 失败。 |
3 | 处理所有其他 lua_pcall 失败。 |
点击此链接查看完整程序:lua_callback_result.cpp。 |
将 Lua 与 C++ 一起使用时,我们需要保护 Lua 解释器免受可能从安装为 lua_CFunction 回调的 C++ 函数抛出的异常的影响。这是本节中的程序,经过重写以使用 C++ 异常(而不是 leaf::result )来安全地将错误从 do_work 函数传达出来:lua_callback_exceptions.cpp。 |
诊断信息
LEAF 能够自动生成诊断消息,其中包括有关错误处理程序可用的所有错误对象的信息
enum class error_code
{
read_error,
write_error
};
....
leaf::try_handle_all(
[]() -> leaf::result<void> (1)
{
...
return leaf::new_error( error_code::write_error, leaf::e_file_name{ "file.txt" } );
},
[]( leaf::match<error_code, error_code::read_error> ) (2)
{
std::cerr << "Read error!" << std::endl;
},
[]( leaf::diagnostic_details const & info ) (3)
{
std::cerr << "Unrecognized error detected\n" << info;
} );
1 | 我们处理在此 try 块中发生的所有失败。 |
2 | 一个或多个应该处理所有可能失败的错误处理程序。 |
3 | try_handle_all 需要这个“捕获所有”错误处理程序。如果 LEAF 无法使用另一个错误处理程序,则将调用它。 |
上面代码片段的 diagnostic_details
输出告诉我们,我们得到了一个值为 1
(write_error
) 的 error_code
,以及一个类型为 e_file_name
的对象,其 .value
中存储了 "file.txt"
Unrecognized error detected Error with serial #1 Caught: error_code: 1 Diagnostic details: boost::leaf::e_file_name: file.txt
在 diagnostic_details 输出中,Caught: 下的部分列出了错误处理程序作为参数接收的对象 — 这些是存储在堆栈上的对象。Diagnostic details: 下的部分列出了所有其他已传达的对象。如果我们没有提供一个接受 diagnostic_details 的处理程序,这些对象将被丢弃。 |
为了打印每个错误对象,LEAF 尝试绑定对 operator<<
的非限定调用,传递一个 std::ostream
和错误对象。如果失败,它还会尝试绑定接受错误类型 .value
的 operator<<
。如果这也无法编译,则错误对象值将不会出现在诊断消息中,尽管 LEAF 仍会打印其类型。
即使对于定义了可打印 .value
的错误类型,用户可能仍然希望为封闭的 struct
重载 operator<<
,例如
struct e_errno
{
int value;
friend std::ostream & operator<<( std::ostream & os, e_errno const & e )
{
return os << e.value << ", \"" << strerror(e.value) << '"';
}
};
上面的 e_errno
类型旨在保存 errno
值。定义的 operator<<
重载将在打印 e_errno
值时自动包含来自 strerror
的输出(LEAF 在 <boost/leaf/common.hpp>
中定义了 e_errno
,以及其他常用的错误类型)。
使用 diagnostic_details
是有代价的。通常,当程序尝试传达当前调用堆栈中的任何错误处理范围中未使用的类型的错误对象时,它们会被丢弃,从而节省周期。但是,如果提供了接受 diagnostic_details
参数的错误处理程序,则此类对象将存储在堆上而不是被丢弃。
如果认为处理 diagnostic_details
的成本太高,请改用 diagnostic_info
leaf::try_handle_all(
[]() -> leaf::result<void>
{
...
return leaf::new_error( error_code::write_error, leaf::e_file_name{ "file.txt" } );
},
[]( leaf::match<error_code, error_code::read_error> )
{
std::cerr << "Read error!" << std::endl;
},
[]( leaf::diagnostic_info const & info )
{
std::cerr << "Unrecognized error detected\n" << info;
} );
在这种情况下,输出可能如下所示
Unrecognized error detected Error serial #1 Caught: error_code: 1
请注意,我们是如何缺少 Diagnostic details:
部分的。那是因为 e_file_name
对象已被 LEAF 丢弃,因为没有错误处理程序需要它。
自动生成的诊断消息对开发者友好,但对用户不友好。 |
使用 std::error_code
, std::error_condition
简介
从阅读标准规范中不易理解 std::error_code
和 std::error_condition
之间的关系。本节解释了它们应该如何使用,以及 LEAF 如何与它们交互。
std::error_code
背后的思想是编码表示错误代码的整数值,以及该值的域。域由 std::error_category
引用表示。从概念上讲,std::error_code
就像一个 pair<std::error_category const &, int>
。
假设我们有这个 enum
enum class libfoo_error
{
e1 = 1,
e2,
e3
};
我们希望能够在 std::error_code
对象中传输 libfoo_error
值。这会擦除它们的静态类型,使其能够跨 API 边界自由传递。为此,我们必须定义一个 std::error_category
来表示我们的 libfoo_error
类型
std::error_category const & libfoo_error_category()
{
struct category: std::error_category
{
char const * name() const noexcept override
{
return "libfoo";
}
std::string message(int code) const override
{
switch( libfoo_error(code) )
{
case libfoo_error::e1: return "e1";
case libfoo_error::e2: return "e2";
case libfoo_error::e3: return "e3";
default: return "error";
}
}
};
static category c;
return c;
}
我们还需要通知标准库,libfoo_error
与 std::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>::value
为 true
,然后使用 make_error_code
创建一个 std::error_code
对象,它实际上用于与 ec
进行比较。
到目前为止还不错,但请记住,标准库还定义了另一种类型,即 std::error_condition
。第一个令人困惑的事情是,就其物理表示而言,std::error_condition
与 std::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
来定义由 libfoo
和 libbar
传达的 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 | 两个逻辑错误条件 c1 和 c2 的枚举。 |
2 | 为表示 my_error_condition 的 std::error_condition 对象定义 std::error_category 。 |
3 | 在这里,我们指定 libfoo:error::e1 、libbar_error::e3 和 libbar_error::e4 中的任何一个在逻辑上都等同于 my_error_condition::c1 ,并且… |
4 | …libfoo:error::e2 、libbar_error::e1 和 libbar_error::e2 中的任何一个在逻辑上都等同于 my_error_condition::c2 。 |
5 | 此特化告诉标准库,my_error_condition 枚举旨在与 std::error_condition 一起使用。 |
6 | 从 my_error_condition 值创建 std::error_condition 对象的工厂函数。 |
呼!
现在,如果我们有一个 std::error_code
对象 ec
,我们可以轻松检查它是否等同于 my_error_condition::c1
,如下所示
if( ec == my_error_condition::c1 )
{
// We have a c1 in our hands
}
再次记住,除了为用 my_error_condition
值初始化的 std::error_condition
对象定义 std::error_category
之外,我们不需要与实际的 std::error_condition
实例交互:它们在需要与 std::error_code
进行比较时创建,这几乎是它们的所有用途。
LEAF 中的支持
match
谓词可以用作 LEAF 错误处理程序的参数,以将 std::error_code
与给定的错误条件匹配。例如,要处理 my_error_condition::c1
(见上文),我们可以使用
leaf::try_handle_some(
[]
{
return f(); // returns leaf::result<T>
},
[]( leaf::match<std::error_code, my_error_condition::c1> m )
{
assert(m.matched == my_error_condition::c1);
....
} );
有关更多示例,请参阅 match
。
Boost 异常集成
除了 Boost Exception 定义的 boost::get_error_info
API 之外,还可以直接使用 LEAF 错误处理程序。考虑以下 boost::get_error_info
的用法
typedef boost::error_info<struct my_info_, int> my_info;
void f(); // Throws using boost::throw_exception
void g()
{
try
{
f();
},
catch( boost::exception & e )
{
if( int const * x = boost::get_error_info<my_info>(e) )
std::cerr << "Got my_info with value = " << *x;
} );
}
我们可以重写 g
以使用 LEAF 访问 my_info
#include <boost/leaf/handle_errors.hpp>
void g()
{
leaf::try_catch(
[]
{
f();
},
[]( my_info x )
{
std::cerr << "Got my_info with value = " << x.value();
} );
}
接受 my_info
意味着只有当捕获的异常对象携带 my_info
时,才会选择处理程序(LEAF 通过 boost::get_error_info
访问 my_info
)。
也支持使用 match
void g()
{
leaf::try_catch(
[]
{
f();
},
[]( leaf::match_value<my_info, 42> )
{
std::cerr << "Got my_info with value = 42";
} );
}
在上面,如果捕获的异常对象携带 my_info
且 .value()
等于 42,则将选择处理程序。
示例
参见 github。
概要
本节列出了 LEAF 中的每个公共头文件,并记录了它提供的定义。
LEAF 标头旨在最大限度地减少耦合
-
报告或转发但不处理错误所需的标头比提供错误处理功能的标头更轻量。
-
提供异常处理或抛出功能的标头与提供错误处理或报告但不使用异常的标头是分开的。
提供独立的单标头选项;请参阅 Distribution。
错误报告
common.hpp
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
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
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
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
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
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
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
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
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;
} }
参考:match
| match_value
| match_member
| catch_
| if_not
| category
| condition
to_variant.hpp
namespace boost { namespace leaf {
// Requires at least C++17
template <class... E, class TryBlock>
std::variant<
typename std::decay<decltype(std::declval<TryBlock>()().value())>::type
std::tuple<
std::optional<E>...>>
to_variant( TryBlock && try_block );
} }
参考:to_variant
参考:函数
每个“参考”部分的内容均按字母顺序组织。 |
activate_context
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
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
namespace boost { namespace leaf {
error_id current_error() noexcept;
} }
- 返回值
-
上次从调用线程调用
new_error
时返回的error_id
值。
另请参阅 on_error 。 |
exception_to_result
namespace boost { namespace leaf {
template <class... Ex, class F>
<<result<T>-deduced>> exception_to_result( F && f ) noexcept;
} }
此函数可用于捕获来自较低级别库的异常,并将其转换为
。result
<T>
- 返回值
-
如果
f
返回类型T
,则exception_to_result
返回leaf::result<T>
。 - 效果
-
-
捕获所有异常,然后在
std::exception_ptr
对象中捕获std::current_exception
,该对象与返回的result<T>
一起 加载。 -
尝试使用
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_type1
或 ex_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
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
namespace boost { namespace leaf {
template <class... Item>
error_id new_error(Item && ... item) noexcept;
} }
- 要求
-
每个
Item…
类型都必须是 no-throw movable 的。 - 效果
-
如同
error_id id = <<generate-new-unique-id>>; return id.load(std::forward<Item>(item)...);
- 返回值
-
一个新的
error_id
值,该值在整个程序中是唯一的。 - 确保
-
id.value()!=0
,其中id
是返回的error_id
。
new_error 丢弃在任何活动错误处理调用范围中未使用的错误对象。 |
当加载到 context 中时,类型为 E 的错误对象将覆盖先前加载的类型为 E 的对象(如果有)。 |
on_error
namespace boost { namespace leaf {
template <class... Item>
<<unspecified-type>> on_error(Item && ... item) noexcept;
} }
- 要求
-
每个
Item…
类型都必须是 no-throw movable 的。 - 效果
-
所有
item…
对象都会被转发和存储,以及从std::unhandled_exceptions
返回的值,存储到未指定类型的返回对象中,该对象应由auto
捕获并在调用范围内保持活动状态。当该对象被销毁时,如果自调用on_error
以来发生了错误,LEAF 将处理存储的项目以获取要与失败关联的错误对象。发生错误时,LEAF 首先需要推导出一个
error_id
值err
,以便将错误对象与之关联。这是使用以下逻辑完成的-
如果自创建
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
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 ,使得
|
2 | 如果第一个参数不是 error_id 类型且不是异常对象,则选择。在这种情况下,抛出的异常是未指定类型,它公开派生自 std::exception 和 类 error_id ,使得
|
3 | 如果调用函数时不带参数,则抛出的异常是未指定类型,它公开派生自 std::exception 和 类 error_id ,使得
|
4 | 如果第一个参数是 error_id 类型,第二个参数是异常对象,即当且仅当 Ex 公开派生自 std::exception 时选择。在这种情况下,抛出的异常是未指定类型,它公开派生自 Ex 和 类 error_id ,使得
|
5 | 如果第一个参数是 error_id 类型,第二个参数不是异常对象,则选择。在这种情况下,抛出的异常是未指定类型,它公开派生自 std::exception 和 类 error_id ,使得
|
6 | 如果仅使用 error_id 对象调用 exception ,则抛出的异常是未指定类型,它公开派生自 std::exception 和 类 error_id ,使得
|
前三个重载抛出一个与新的 error_id 关联的异常对象。后三个重载抛出一个与指定的 error_id 关联的异常对象。 |
struct my_exception: std::exception { };
leaf::throw_exception(my_exception{}); (1)
1 | 抛出一个类型派生自 error_id 和 my_exception 的异常(因为 my_exception 派生自 std::exception )。 |
enum class my_error { e1=1, e2, e3 }; (1)
leaf::throw_exception(my_error::e1);
1 | 抛出一个类型派生自 error_id 和 std::exception 的异常(因为 my_error 不派生自 std::exception )。 |
要自动捕获 __FILE__ 、__LINE__ 和 __FUNCTION__ 与返回的对象,请使用 BOOST_LEAF_THROW_EXCEPTION 而不是 leaf::throw_exception 。 |
to_variant
namespace boost { namespace leaf {
template <class... E, class TryBlock>
std::variant<
typename std::decay<decltype(std::declval<TryBlock>()().value())>::type
std::tuple<
std::optional<E>...>>
to_variant( TryBlock && try_block );
} }
- 要求
-
-
此功能仅在 C++-17 或更新版本下可用。
-
try_block
函数可能不带任何参数。 -
try_block
函数返回的类型必须是result<T>
类型(参见is_result_type
)。try_block
返回leaf::
是有效的,但这并非必需。result
<T>
-
to_variant
函数在内部使用 try_handle_all
来调用 try_block
并将结果捕获到 std::variant
中。成功时,variant 包含来自生成的 result<T>
的 T
对象。否则,variant 包含一个 std::tuple
,其中每个 std::optional
元素包含用户提供的序列 E…
中类型为 Ei
的对象,或者如果失败未生成该类型的错误对象,则为空。
enum class E1 { e11, e12, e13 };
enum class E2 { e21, e22, e23 };
enum class E3 { e31, e32, e33 };
....
auto v = leaf::to_variant<E1, E2, E3>(
[]() -> leaf::result<int>
{
return leaf::new_error( E1::e12, E3::e33 );
} );
assert(v.index() == 1); (1)
auto t = std::get<1>(v); (2)
assert(std::get<0>(t).value() == E1::e12); (3)
assert(!std::get<1>(t).has_value()); (4)
assert(std::get<2>(t).value() == E3::e33); (3)
1 | 我们报告一个失败,因此 variant 必须包含错误对象元组,而不是 int 。 |
2 | 获取错误元组。 |
3 | 我们传达了一个 E1 和一个 E3 错误对象… |
4 | …但没有 E2 错误对象。 |
try_capture_all
#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::
的实例,其中 T 根据result
<T>TryBlock
的返回类型R
推导出来-
如果
R
是某种类型Result<T>
,对于该类型is_result_type
为真,则try_capture_all
返回leaf::
。result
<T> -
否则,假定
TryBlock
通过抛出异常来报告错误,并且try_capture_all
的返回值被推导为leaf::result<R>
。
-
- 效果
-
try_capture_all
执行try_block
,捕获并捕获返回的leaf::result
对象中的所有异常和所有传达的错误对象。错误对象是动态分配的。
对 try_capture_all 的调用不得嵌套在 try_handle_all /try_handle_some /try_catch 或另一个 try_capture_all 中。 |
在 BOOST_LEAF_CFG_CAPTURE=0 下,try_capture_all 不可用。 |
- 另请参阅
try_catch
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
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
namespace boost { namespace leaf {
template <class TryBlock, class... H>
typename std::decay<decltype(std::declval<TryBlock>()())>::type
try_handle_some( TryBlock && try_block, H && ... h );
} }
- 要求
-
-
try_block
函数可能不带任何参数。 -
try_block
函数返回的类型必须是result<T>
类型(参见is_result_type
)。try_block
返回leaf::
是有效的,但这并非必需。result
<T> -
每个
h…
函数-
必须返回可用于初始化类型
R
的对象的类型;如果 R 是result<void>
(即,在成功的情况下,它不传达值),则允许返回void
的处理程序。如果选择了这样的处理程序,则try_handle_some
返回值由{}
初始化; -
可以按值、按(
const
)引用或作为指针(指向const
)获取任何错误对象; -
可以按值获取任何谓词类型的参数:
catch_
、match
、match_value
、match_member
、if_not
,或任何用户定义的谓词类型Pred
,对于该类型
为is_predicate
<Pred>::valuetrue
; -
可以按
const &
获取error_info
参数; -
可以按
const &
获取diagnostic_info
参数; -
可以按
const &
获取diagnostic_details
参数。
-
-
- 效果
-
-
创建一个本地
对象context
<E…>ctx
,其中E…
类型是从每个h…
接受的参数类型自动推导出来的,这保证了ctx
能够存储处理错误所需的所有类型。 -
调用
try_block
-
如果返回的对象
r
指示成功 并且try_block
没有抛出异常,则r
将转发给调用方。 -
否则,LEAF 会按顺序考虑每个
h…
处理程序,直到找到一个可以使用当前存储在ctx
中,与r.error()
关联的错误对象为其提供参数的处理程序。第一个这样的处理程序被调用,并且其返回值用于初始化try_handle_some
的返回值,如果处理程序能够处理错误,则返回值可以指示成功,如果不能,则指示失败。 -
如果
try_handle_some
无法找到合适的处理程序,则返回r
。
-
-
try_handle_some 是异常中立的:它不会抛出异常,但是允许 try_block 和任何 h… 抛出异常。 |
- 处理程序选择过程
-
一个处理程序
h
适合处理r
报告的失败,当且仅当try_handle_some
能够生成值作为其参数传递,使用当前在ctx
中可用的错误对象,这些错误对象与通过调用r.error()
获取的错误 ID 相关联。一旦确定无法生成参数值,则会丢弃当前处理程序,并继续选择下一个处理程序(如果有)。r.error()
的返回值必须可以隐式转换为error_id
。当然,leaf::result
模板满足此要求。如果使用外部result
类型,通常r.error()
会返回一个std::error_code
,它可以传递 LEAF 错误 ID;请参阅 互操作性。如果
err
是从r.error()
获取的error_id
,则当前正在考虑的处理程序所接受的每个参数ai
的生成方式如下-
如果
ai
的类型为Ai
、Ai const&
或Ai&
-
如果类型为
Ai
的错误对象(与err
关联)当前在ctx
中可用,则ai
将初始化为对该对象的引用;否则 -
如果
Ai
派生自std::exception
,并且try_block
抛出一个派生自std::exception
的类型的对象ex
,则 LEAF 获取Ai* p = dynamic_cast<Ai*>(&ex)
。如果p
为空,则丢弃处理程序,否则ai
初始化为*p
。 -
否则,丢弃处理程序。
示例.... auto r = leaf::try_handle_some( []() -> leaf::result<int> { return f(); }, [](leaf::e_file_name const & fn) (1) { std::cerr << "File Name: \"" << fn.value << '"' << std::endl; (2) return 1; } );
1 如果 try_block
指示失败,则当ctx
存储与错误关联的e_file_name
时,将选择此处理程序。由于这是唯一提供的处理程序,如果e_file_name
不可用,try_handle_some
将返回由f
返回的leaf::result<int>
。2 打印文件名,处理错误。 -
-
如果
ai
的类型为Ai
const*
或Ai*
,则try_handle_some
始终能够生成它:首先它尝试像按引用获取那样生成它;如果失败,则不是丢弃处理程序,而是将ai
初始化为0
。示例.... try_handle_some( []() -> leaf::result<int> { return f(); }, [](leaf::e_file_name const * fn) (1) { if( fn ) (2) std::cerr << "File Name: \"" << fn->value << '"' << std::endl; return 1; } ); }
1 此处理程序可以被选择来处理任何错误,因为它将 e_file_name
作为const *
(并且没有其他)。2 如果当前错误有 e_file_name
可用,则打印它。 -
如果
ai
的类型是谓词类型Pred
(对于该类型,
为is_predicate
<Pred>::valuetrue
),则E
推导为typename Pred::error_type
,然后-
如果
E
不是void
,并且类型为E
的错误对象e
(与err
关联)当前未存储在ctx
中,则丢弃处理程序;否则,如果表达式Pred::evaluate(e)
返回false
,则丢弃处理程序。 -
如果
E
是void
,并且没有捕获std::exception
,则丢弃处理程序;否则,如果表达式Pred::evaluate(e)
(其中e
的类型为std::exception const &
)返回false
,则丢弃处理程序。 -
要调用处理程序,
Pred
参数ai
使用Pred{e}
初始化。另请参阅:谓词。
-
-
如果
ai
的类型为error_info const &
,则try_handle_some
始终能够生成它。示例.... try_handle_some( [] { return f(); // returns leaf::result<T> }, [](leaf::error_info const & info) (1) { std::cerr << "leaf::error_info:\n" << info; (2) return info.error(); (3) } );
1 此处理程序匹配任何错误。 2 打印错误信息。 3 返回原始错误,它将从 try_handle_some
中返回。 -
如果
ai
的类型为diagnostic_info const &
,则try_handle_some
始终能够生成它。示例.... try_handle_some( [] { return f(); // throws }, [](leaf::diagnostic_info const & info) (1) { std::cerr << "leaf::diagnostic_information:\n" << info; (2) return info.error(); (3) } );
1 此处理程序匹配任何错误。 2 打印诊断信息,包括有关已丢弃错误对象的有限信息。 3 返回原始错误,它将从 try_handle_some
中返回。 -
如果
ai
的类型为diagnostic_details const &
,则try_handle_some
始终能够生成它。示例.... try_handle_some( [] { return f(); // throws }, [](leaf::diagnostic_details const & info) (1) { std::cerr << "leaf::diagnostic_details\n" << info; (2) return info.error(); (3) } );
1 此处理程序匹配任何错误。 2 打印详细的诊断信息,包括已丢弃错误对象的值。 3 返回原始错误,它将从 try_handle_some
中返回。
-
参考:类型
每个“参考”部分的内容均按字母顺序组织。 |
context
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;
} }
构造函数 | activate
| deactivate
| is_active
| unload
| print
| handle_error
| context_type_from_handlers
context
类模板为每个指定的 E…
类型提供存储。通常,context
对象不直接使用;当调用 try_handle_some
、try_handle_all
或 try_catch
函数时,会内部创建它们,并使用从传递的处理程序的参数类型自动推导出的类型实例化。
独立地,如果用户需要捕获错误对象然后通过移动 context
对象本身来传输它们,则可以创建 context
对象。
即使在这种情况下,也建议用户不要通过显式列出他们希望 context
能够存储的 E…
类型来实例化 context
模板。而是使用 context_type_from_handlers
或调用 make_context
函数模板,它们从捕获的处理程序函数对象列表中推导出正确的 E…
类型。
为了能够在 context
对象中加载错误对象,必须激活它。激活 context
对象 ctx
会将其绑定到调用线程,并将存储的 E…
类型的线程局部指针设置为指向 ctx
中的相应存储。在一个给定的线程中,有可能甚至很可能拥有多个活动的 context
。在这种情况下,激活/停用必须以 LIFO 方式发生。因此,最好使用 context_activator
,它依赖 RAII 来激活和停用 context
。
当 context
被停用时,它会从调用线程分离,将线程局部指针恢复到其 pre-activate
值。通常,此时,存储的错误对象(如果有)要么被丢弃(默认情况下),要么通过调用 unload
移动到调用线程中其他活动的 context
对象中的相应存储中(如果可用)。
虽然错误处理通常使用 try_handle_some
、try_handle_all
或 try_catch
,但也可以通过调用成员函数 handle_error
来处理错误。它接受一个 error_id
,并尝试根据存储在 *this
中的与传递的 error_id
关联的错误对象来选择错误处理程序。
context 对象可以移动,只要它们不是活动的。 |
移动活动的 context 会导致未定义的行为。 |
构造函数
namespace boost { namespace leaf {
template <class... E>
context<E...>::context() noexcept;
template <class... E>
context<E...>::context( context && x ) noexcept;
} }
默认构造函数初始化一个空的 context
对象:它提供存储,但不包含任何错误对象。
移动构造函数将存储的错误对象从一个 context
移动到另一个 context
。
移动活动的 context 对象会导致未定义的行为。 |
activate
namespace boost { namespace leaf {
template <class... E>
void context<E...>::activate() noexcept;
} }
当上下文与线程关联时,线程局部指针设置为指向其存储中的每个 E…
类型,同时每个此类指针的先前值都保存在 context
对象中,以便可以通过调用 deactivate
来撤消 activate
的效果。
当错误对象被 加载 时,它会被移动到调用线程中最后激活的(在调用线程中)context
对象中,该对象为其类型提供存储(请注意,这可能不是最后激活的 context
对象)。如果没有这样的存储可用,则错误对象将被丢弃。
deactivate
namespace boost { namespace leaf {
template <class... E>
void context<E...>::deactivate() noexcept;
} }
当上下文被停用时,当前指向其中每个单独错误对象存储的线程局部指针将恢复为其在调用 activate
之前的原始值。
handle_error
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
namespace boost { namespace leaf {
template <class... E>
bool context<E...>::is_active() const noexcept;
} }
- 返回值
-
如果
*this
在任何线程中处于活动状态,则为true
,否则为false
。
print
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
namespace boost { namespace leaf {
template <class... E>
void context<E...>::unload( error_id id ) noexcept;
} }
- 要求
-
!
.is_active
() - 效果
-
类型为
E
的每个存储的错误对象都会移动到调用堆栈中另一个活动的context
对象中,如果该对象为类型为E
的对象提供存储(如果有),否则会被丢弃。如果目标对象与指定的id
关联,则不会覆盖它们,除非id.value() == 0
。
context_activator
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_activator
时 ctx.is_active
() 为 true
,则构造函数和析构函数不起作用。否则
-
构造函数在
*this
中存储对ctx
的引用,并调用ctx.activate
()。 -
析构函数
-
如果
ctx.is_active()
为false
,则不起作用(即,可以在context_activator
对象过期之前手动调用deactivate
); -
否则,调用
ctx.deactivate
()。
-
为了自动推导 Ctx
,请使用 activate_context
。
diagnostic_details
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_some
、try_handle_all
或 try_catch
)的处理程序可以接受类型为 diagnostic_details const &
的参数,如果它们需要打印有关错误的诊断信息。
由 operator<<
打印的消息包括由 error_info
打印的消息,后跟有关已传递给 LEAF(与错误关联)的错误对象的信息,这些错误对象在任何活动的 context
中都没有可用的存储(这些错误对象被 LEAF 丢弃,因为没有处理程序需要它们)。
附加信息包括所有此类错误对象的类型和值(但请参阅 show_in_diagnostics
)。
|
使用 diagnostic_details 可能会动态分配内存,但仅当活动错误处理程序接受类型为 diagnostic_details 的参数时才会分配内存。 |
diagnostic_info
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_some
、try_handle_all
或 try_catch
的处理程序可以接受类型为 diagnostic_info const &
的参数,如果它们需要打印有关错误的诊断信息。
由 operator<<
打印的消息包括由 error_info
打印的消息,后跟有关已传递给 LEAF(与错误关联)的错误对象的基本信息,这些错误对象在任何活动的 context
中都没有可用的存储(这些错误对象被 LEAF 丢弃,因为没有处理程序需要它们)。
附加信息仅限于第一个此类错误对象的类型名称以及它们的总计数。
|
error_id
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;
} }
构造函数 | value
| operator bool
| to_error_code
| operator==
, !=
, <
| load
| is_error_id
| new_error
| current_error
error_id
类型的值标识整个程序中特定错误的发生。它们可以被复制、移动、赋值和与其他 error_id
对象进行比较。它们与 int
一样高效。
构造函数
namespace boost { namespace leaf {
error_id::error_id() noexcept = default;
template <class Enum>
error_id::error_id( Enum e, typename std::enable_if<std::is_error_code_enum<Enum>::value, Enum>::type * = 0 ) noexcept;
error_id::error_id( std::error_code const & ec ) noexcept;
} }
默认初始化的 error_id
对象不代表特定错误。它与任何其他默认初始化的 error_id
对象比较相等。所有其他 error_id
对象都标识特定错误的发生。
当使用 error_id 类型的对象初始化 result<T> 对象时,即使传递默认初始化的 error_id 值,它也会在错误状态下初始化。 |
将 error_id
对象转换为 std::error_code
使用 LEAF 识别的未指定的 std::error_category
。这允许 error_id
通过使用 std::error_code
的接口进行传输。std::error_code
构造函数允许恢复原始 error_id
。
要检查给定的 std::error_code 是否实际携带 error_id ,请使用 is_error_id 。 |
通常,用户通过调用 new_error
创建新的 error_id
对象。接受 std::error_code
的构造函数和接受类型为 Enum
的构造函数(对于该类型,std::is_error_code_enum<Enum>::value
为 true
)具有以下效果
-
如果
ec.value()
为0
,则效果与使用默认构造函数相同。 -
否则,如果
为is_error_id
(ec)true
,则原始error_id
值用于初始化*this
; -
否则,
*this
由new_error
返回的值初始化,同时ec
传递给load
,这使得与try_handle_some
、try_handle_all
或try_catch
一起使用的处理程序可以将其接收为类型为std::error_code
的参数。
is_error_id
namespace boost { namespace leaf {
bool is_error_id( std::error_code const & ec ) noexcept;
} }
- 返回值
-
如果
ec
使用 LEAF 特定的std::error_category
(将其标识为携带错误 ID 而不是其他错误代码),则返回true
;否则返回false
。
load
namespace boost { namespace leaf {
template <class... Item>
error_id error_id::load( Item && ... item ) const noexcept;
} }
- 要求
-
每个
Item…
类型都必须是 no-throw movable 的。 - 效果
-
-
如果
this->value()==0
,则所有item…
都将被丢弃,并且不会采取进一步操作。 -
否则,每个
item
的处理方式取决于其类型-
如果它是一个接受类型为
E &
的单个参数的函数,则使用当前与*this
关联的类型为E
的对象调用该函数。如果不存在此类对象,则默认初始化的对象与*this
关联,然后传递给该函数。 -
如果它是一个不接受任何参数的函数,则调用该函数以获取与
*this
关联的错误对象,但在void
函数的特殊情况下,将调用它,并且不获取/加载错误对象。 -
否则,
item
本身被假定为错误对象,它与*this
关联。
-
-
- 返回值
-
*this
.
load 丢弃在任何活动错误处理调用范围内未使用的错误对象。 |
当加载到 context 中时,类型为 E 的错误对象将覆盖先前加载的类型为 E 的对象(如果有)。 |
- 另请参阅
operator==
, !=
, <
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 对象 a 和 b ,a < b 为真,则不一定表示由 a 标识的失败早于由 b 标识的失败发生。 |
operator bool
namespace boost { namespace leaf {
explicit error_id::operator bool() const noexcept;
} }
- 效果
-
如同
return value()!=0
。
to_error_code
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
namespace boost { namespace leaf {
int error_id::value() const noexcept;
} }
- 效果
-
-
如果
*this
是使用默认构造函数初始化的,则返回 0。 -
否则返回一个保证不为 0 的
int
:程序范围内的唯一失败标识符。
-
error_monitor
namespace boost { namespace leaf {
class error_monitor
{
public:
error_monitor() noexcept;
error_id check() const noexcept;
error_id assigned_error_id( E && ... e ) const noexcept;
};
} }
当通过不使用 LEAF 报告错误的非协作 API 增强使用 LEAF 传递的失败时(因此在错误时不会返回 error_id
),此类有助于获取 error_id
以关联错误对象。
此类的一般用法如下
error_code compute_value( int * out_value ) noexcept; (1)
leaf::error<int> augmenter() noexcept
{
leaf::error_monitor cur_err; (2)
int val;
auto ec = compute_value(&val);
if( failure(ec) )
return cur_err.assigned_error_id().load(e1, e2, ...); (3)
else
return val; (4)
}
1 | 非协作第三方 API 不使用 LEAF,但可能会导致调用使用 LEAF 的用户回调。如果我们的回调报告失败,我们将使用调用范围内可用的错误对象来增强它,即使 compute_value 无法传递 error_id 。 |
2 | 初始化 error_monitor 对象。 |
3 | 对 compute_value 的调用失败
|
4 | 调用成功,返回计算值。 |
check
函数的工作方式类似,但它不是调用 new_error
,而是返回默认初始化的 error_id
。
e_api_function
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
namespace boost { namespace leaf {
struct e_at_line { int value; };
} }
e_at_line
可用于在报告有关文本文件(例如解析错误)的错误时传递行号。
e_errno
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
namespace boost { namespace leaf {
struct e_file_name { std::string value; };
} }
当文件操作失败时,您可以使用 e_file_name
来存储文件名。
最好定义您自己的文件名包装器,以避免如果不同的模块都使用 leaf::e_file_name 而发生冲突。最好使用描述性名称来阐明它是哪种类型的文件名(例如 e_source_file_name 、e_destination_file_name ),或者至少在给定模块的命名空间中定义 e_file_name 。 |
e_LastError
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
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_ERROR
和 BOOST_LEAF_THROW_EXCEPTION
宏将 __FILE__
、__LINE__
和 __FUNCTION__
捕获到 e_source_location
对象中。
e_type_info_name
namespace boost { namespace leaf {
struct e_type_info_name { char const * value; };
} }
e_type_info_name
旨在存储 std::type_info::name
的返回值。
error_info
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_some
、try_handle_all
或 try_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_some
、try_handle_all
或 try_catch
作用域,但仅当它与由 error()
返回的 error_id
关联时才打印。
result
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
{
};
} }
构造函数 | operator=
| has_value
| has_error
| operator bool
| value
| operator->
| operator*
| error
| load
result<T>
类型可以由生成类型为 T
的值但可能无法执行此操作的函数返回。
- 要求
-
T
必须是可移动的,并且其移动构造函数可能不会抛出异常。 - 不变式
-
result<T>
对象处于以下三种状态之一-
值状态,在这种状态下,它包含类型为
T
的对象,并且可以使用value
/operator*
/operator->
访问包含的值。 -
错误状态,在这种状态下,它包含一个错误 ID,并且调用
value
会抛出leaf::bad_result
。 -
动态捕获状态,它与错误状态相同,但除了错误 ID 之外,它还保存动态捕获的错误对象列表;请参阅
try_capture_all
。
-
result<T>
对象是不可抛出移动的,但不可复制。
构造函数
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>
不变式当使用
std::error_code
对象初始化result
对象时,它用于初始化error_id
对象,然后行为与使用error_id
初始化时相同。 - 抛出
-
-
在值状态下初始化
result<T>
可能会抛出异常,具体取决于调用T
的哪个构造函数; -
其他构造函数不会抛出异常。
-
处于值状态的 result 在布尔上下文中转换为 true 。未处于值状态的 result 在布尔上下文中转换为 false 。 |
result<T> 对象是不可抛出移动的,但不可复制。 |
error
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
的状态。
返回的代理对象引用 *this ;避免持有它。 |
load
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=
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
namespace boost { namespace leaf {
template <class T>
bool result<T>::has_value() const noexcept;
} }
- 返回值
-
如果
*this
处于 值状态,则返回true
,否则返回false
。
has_error
namespace boost { namespace leaf {
template <class T>
bool result<T>::has_error() const noexcept;
} }
- 返回值
-
如果
*this
处于 值状态,则返回false
,否则返回true
。
operator bool
namespace boost { namespace leaf {
template <class T>
result<T>::operator bool() const noexcept;
} }
- 返回值
-
如果
*this
处于 值状态,则返回true
,否则返回false
。
value
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
包含捕获的异常对象ex
,则行为等同于
。throw_exception
(ex) -
否则,行为等同于
。throw_exception
(bad_result{})
-
-
如果
*this
处于任何其他状态,则行为等同于
。throw_exception
(bad_result{})
operator->
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*
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
namespace boost { namespace leaf {
template <class E>
struct show_in_diagnostics: std::true_type
{
};
} }
可以专门化此模板,以防止敏感类型的错误对象出现在自动生成的诊断消息中。示例
struct e_user_name
{
std::string value;
};
namespace boost { namespace leaf {
template <>
struct show_in_diagnostics<e_user_name>: std::false_type
{
};
} }
参考:谓词
每个“参考”部分的内容均按字母顺序组织。 |
以下谓词可用
此外,任何用户定义的类型 Pred
,只要
为 is_predicate
<Pred>::valuetrue
,都将被视为谓词。在这种情况下,需要满足以下条件:
-
Pred
定义一个可访问的成员类型error_type
,以指定其需要的错误对象类型; -
Pred
定义一个可访问的静态成员函数evaluate
,该函数返回布尔类型,并且可以使用类型为error_type const &
的对象调用; -
Pred
实例可以使用类型为error_type
的对象进行初始化。
当错误处理程序接受谓词类型 Pred
的参数时,如果类型为 Pred::error_type
的错误对象 e
不可用,则 处理程序选择过程 将丢弃该处理程序。否则,如果 Pred::evaluate(e)
返回 false
,则也会丢弃该处理程序。如果调用了处理程序,则使用 Pred{e}
初始化 Pred
参数。
谓词在错误处理程序被调用之前进行评估,因此它们可能无法访问动态状态(当然,错误处理程序本身可以访问动态状态,例如通过 lambda 表达式捕获)。 |
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 错误处理程序参数。 |
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_
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 。 |
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 抛出类型为 ex1 或 ex2 的异常,则选择该处理程序。 |
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
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 的对象,它与 e1 或 e2 比较 不 相等,并且 与 检测到的错误相关联,则选择该处理程序。 |
match
namespace boost { namespace leaf {
template <class E, auto... V>
class match
{
<<deduced>> matched;
// Other members not specified
};
template <class E, auto... V>
struct is_predicate<match<E, V...>>: std::true_type
{
};
} }
当错误处理程序接受类型为 match<E, V…>
的参数时,处理程序选择过程 首先检查类型为 E
的错误对象 e
是否可用。如果不可用,则丢弃该处理程序。否则,如果未满足以下条件,则也会丢弃该处理程序
p1 || p2 || … pn
.
其中 pi
等效于 e == Vi
,除非 Vi
是指向函数的指针
bool (*Vi)(T x)
.
在这种情况下,需要 Vi != 0
并且 x
可以使用 E const &
初始化,然后 pi
等效于
Vi(e)
.
特别是,对于任何 Vi
,传递指向函数 leaf::category<Enum>
的指针是有效的,其中
std::is_error_code_enum<Enum>::value || std::is_error_condition_enum<Enum>::value
.
在这种情况下,pi
等效于
&e.category() == &std::error_code(Enum{}).category()
.
如果调用了错误处理程序,则可以使用 matched
访问 e
。
另请参阅 使用谓词处理错误。 |
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 的对象,它与 e1 或 e2 比较相等,并且与检测到的错误相关联,则选择该处理程序。 |
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 的对象,它与 e1 或 e2 比较相等,并且与检测到的错误相关联,则选择该处理程序。 |
enum class enum_a { a1=1, a2, a3 };
enum class enum_b { b1=1, b2, b3 };
namespace std
{
template <> struct is_error_code_enum<enum_a>: std::true_type { };
template <> struct is_error_code_enum<enum_b>: std::true_type { };
}
leaf::try_handle_some(
[]
{
return f(); // returns leaf::result<T>
},
[]( leaf::match<std::error_code, leaf::category<enum_a>, enum_b::b2> m )
{ (1)
static_assert(std::is_same<std::error_code const &, decltype(m.matched)>::value);
assert(&m.matched.category() == &std::error_code(enum_{}).category() || m.matched == enum_b::b2);
....
} );
1 | 如果类型为 std::error_code 的对象,它要么具有与 enum_a 相同的 std::error_category ,要么与 enum_b::b2 比较相等,并且与检测到的错误相关联,则选择该处理程序。 |
leaf::category
模板的使用需要自动推导每个 Vi
的类型,这反过来又需要 C++17 或更高版本。将 std::error_code
用作 E
也是如此,但 LEAF 为这种情况提供了兼容 C++11 的解决方法,使用模板 condition
。以下内容等效于示例 2
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
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
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
namespace boost { namespace leaf {
template <class E, auto... V>
struct match_value
{
E const & matched;
// Other members not specified
};
template <class E, auto... V>
struct is_predicate<match_value<E, V...>>: std::true_type
{
};
} }
此谓词类似于 match
,但 match
将可用的类型为 E
的错误对象 e
与指定的值 V…
进行比较,而 match_value
则使用 e.value
。
另请参阅 使用谓词处理错误。 |
struct e_errno { int value; }
leaf::try_handle_some(
[]
{
return f(); // returns leaf::result<T>
},
[]( leaf::match_value<e_errno, ENOENT> m )
{ (1)
static_assert(std::is_same<e_errno const &, decltype(m.matched)>::value);
assert(m.matched.value == ENOENT);
....
} );
1 | 如果类型为 e_errno 的对象,其 .value 等于 ENOENT ,并且与检测到的错误相关联,则选择该处理程序。 |
参考:特性
每个“参考”部分的内容均按字母顺序组织。 |
is_predicate
namespace boost { namespace leaf {
template <class T>
struct is_predicate: std::false_type
{
};
} }
is_result_type
namespace boost { namespace leaf {
template <class R>
struct is_result_type: std::false_type
{
};
} }
try_handle_some
和 try_handle_all
提供的错误处理功能 — 包括 加载 任意类型的错误对象的能力 — 与任何外部 result<T>
类型 R 兼容,只要对于给定类型为 R
的对象 r
满足以下条件:
-
如果
bool(r)
为true
,则r
表示成功,在这种情况下,调用r.value()
以恢复T
值是有效的。 -
否则,
r
表示失败,在这种情况下,调用r.error()
是有效的。返回的值用于初始化error_id
(注意:error_id
可以通过std::error_code
初始化)。
要使用外部 result<T>
类型 R,您必须特化 is_result_type
模板,以便 is_result_type<R>::value
的评估结果为 true
。
当然,提供的 leaf::
类模板满足这些要求。此外,它允许使用 result
<T>try_capture_all
在线程边界之间传输错误对象。
参考:宏
每个“参考”部分的内容均按字母顺序组织。 |
BOOST_LEAF_ASSIGN
#define BOOST_LEAF_ASSIGN(v, r)\
auto && <<temp>> = r;\
if( !<<temp>> )\
return <<temp>>.error();\
v = std::forward<decltype(<<temp>>)>(<<temp>>).value()
当调用返回 result<T>
(而不是 result<void>
)的函数时,如果期望的行为是将任何错误原封不动地转发给调用方,则 BOOST_LEAF_ASSIGN
非常有用。
如果成功,类型为 T
的结果 value()
将分配给指定的变量 v
,该变量必须在调用 BOOST_LEAF_ASSIGN
之前声明。但是,可以使用 BOOST_LEAF_ASSIGN
声明新变量,方法是在 v
中传递其类型及其名称,例如 BOOST_LEAF_ASSIGN(auto && x, f())
调用 f
,将错误转发给调用方,同时将成功的值捕获到 x
中。
另请参阅 BOOST_LEAF_AUTO 。 |
BOOST_LEAF_AUTO
#define BOOST_LEAF_AUTO(v, r)\
BOOST_LEAF_ASSIGN(auto v, r)
当调用返回 result<T>
(而不是 result<void>
)的函数时,如果期望的行为是将任何错误原封不动地转发给调用方,则 BOOST_LEAF_AUTO
非常有用。
leaf::result<int> compute_value();
leaf::result<float> add_values()
{
BOOST_LEAF_AUTO(v1, compute_value()); (1)
BOOST_LEAF_AUTO(v2, compute_value()); (2)
return v1 + v2;
}
1 | 调用 compute_value ,在失败时跳出,在成功时定义一个局部变量 v1 。 |
2 | 再次调用 compute_value ,在失败时跳出,在成功时定义一个局部变量 v2 。 |
当然,我们可以不使用 BOOST_LEAF_AUTO
编写 add_value
。这等效于
leaf::result<float> add_values() { auto v1 = compute_value(); if( !v1 ) return v1.error(); auto v2 = compute_value(); if( !v2 ) return v2.error(); return v1.value() + v2.value(); }
另请参阅 BOOST_LEAF_ASSIGN 。 |
BOOST_LEAF_CHECK
#if BOOST_LEAF_CFG_GNUC_STMTEXPR
#define BOOST_LEAF_CHECK(r)\
({\
auto && <<temp>> = (r);\
if( !<<temp>> )\
return <<temp>>.error();\
std::move(<<temp>>);\
}).value()
#else
#define BOOST_LEAF_CHECK(r)\
{\
auto && <<temp>> = (r);\
if( !<<temp>> )\
return <<temp>>.error();\
}
#endif
当调用返回 result<void>
的函数时,如果期望的行为是将任何错误原封不动地转发给调用方,则 BOOST_LEAF_CHECK
非常有用。
leaf::result<void> send_message( char const * msg );
leaf::result<int> compute_value();
leaf::result<int> say_hello_and_compute_value()
{
BOOST_LEAF_CHECK(send_message("Hello!")); (1)
return compute_value();
}
1 | 尝试发送消息,然后计算值,使用 BOOST_LEAF_CHECK 报告错误。 |
不使用 BOOST_LEAF_CHECK
的等效实现
leaf::result<float> add_values() { auto r = send_message("Hello!"); if( !r ) return r.error(); return compute_value(); }
如果 BOOST_LEAF_CFG_GNUC_STMTEXPR
为 1
(在 __GNUC__
下这是默认值),则 BOOST_LEAF_CHECK
扩展为 GNU C 语句表达式,这允许将其与任何表达式中的非 void
结果类型一起使用;请参阅 检查错误。
BOOST_LEAF_THROW_EXCEPTION
#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
#define BOOST_LEAF_NEW_ERROR <<exact-definition-unspecified>>
- 效果
-
BOOST_LEAF_NEW_ERROR(e…)
等效于leaf::
,只是当前源位置会自动传递,位于new_error
(e…)
对象中(除了所有e_source_location
e…
对象之外)。
配置
识别以下配置宏
-
BOOST_LEAF_CFG_DIAGNOSTICS
:将此宏定义为0
会桩化diagnostic_info
和diagnostic_details
(如果宏保持未定义,则 LEAF 将其定义为1
)。 -
BOOST_LEAF_CFG_STD_SYSTEM_ERROR
:将此宏定义为0
会禁用std::error_code
/std::error_condition
集成。在这种情况下,LEAF 不会#include <system_error>
,这对于嵌入式平台来说可能太重了(如果宏保持未定义,则 LEAF 将其定义为1
)。 -
BOOST_LEAF_CFG_STD_STRING
:将此宏定义为0
会禁用所有std::string
的使用(这也需要BOOST_LEAF_CFG_DIAGNOSTICS=0
)。在这种情况下,LEAF 不会#include <string>
,这对于嵌入式平台来说可能太重了(如果宏保持未定义,则 LEAF 将其定义为1
)。 -
BOOST_LEAF_CFG_CAPTURE
:将此宏定义为0
会禁用try_capture_all
,它(仅在使用时)会动态分配内存(如果宏保持未定义,则 LEAF 将其定义为1
)。 -
BOOST_LEAF_CFG_GNUC_STMTEXPR
:此宏控制是否根据 GNU C 语句表达式 定义BOOST_LEAF_CHECK
,这使其能够用于检查错误,类似于问号运算符在某些语言中的工作方式(请参阅 检查错误)。默认情况下,在__GNUC__
下宏定义为1
,否则定义为0
。 -
BOOST_LEAF_CFG_WIN32
:将此宏定义为 1 会启用e_LastError
中的默认构造函数,以及在打印diagnostic_details
时自动转换为字符串(通过FormatMessageA
)。如果宏保持未定义,则 LEAF 将其定义为0
(即使在 Windows 上,因为通常不希望包含windows.h
)。请注意,e_LastError
类型本身在所有平台上都可用,使用它的错误处理程序无需条件编译。 -
BOOST_LEAF_NO_EXCEPTIONS
:禁用所有异常处理支持。如果保持未定义,LEAF 会根据编译器配置(例如-fno-exceptions
)自动定义它。 -
BOOST_LEAF_NO_THREADS
:禁用 LEAF 中的所有线程安全。
配置 TLS 访问
LEAF 需要支持线程本地 void
指针。默认情况下,这是通过 C++11 thread_local
关键字实现的,但为了支持 嵌入式平台,可以将 LEAF 配置为使用线程本地指针数组,方法是定义 BOOST_LEAF_USE_TLS_ARRAY
。在这种情况下,用户需要定义以下两个函数来实现所需的 TLS 访问
namespace boost { namespace leaf {
namespace tls
{
void * read_void_ptr( int tls_index ) noexcept;
void write_void_ptr( int tls_index, void * p ) noexcept;
}
} }
为了提高效率,read_void_ptr 和 write_void_ptr 应定义为 inline 。 |
在 BOOST_LEAF_USE_TLS_ARRAY
下,识别以下附加配置宏
-
BOOST_LEAF_CFG_TLS_ARRAY_START_INDEX
指定 LEAF 可用的起始 TLS 数组索引(如果宏保持未定义,则 LEAF 将其定义为0
)。 -
可以定义
BOOST_LEAF_CFG_TLS_ARRAY_SIZE
以指定 TLS 数组的大小。在这种情况下,TLS 索引在传递给read_void_ptr
/write_void_ptr
之前,会通过BOOST_LEAF_ASSERT
进行验证。 -
可以定义
BOOST_LEAF_CFG_TLS_INDEX_TYPE
以指定用于存储分配的 TLS 索引的整数类型(如果宏保持未定义,则 LEAF 将其定义为unsigned char
)。
报告程序不用于处理故障的类型的错误对象不会消耗 TLS 指针。LEAF 所需的 TLS 指针数组的最小大小是用作错误处理程序参数(在整个程序中)的不同类型的总数加一。 |
请注意 read_void_ptr /write_void_ptr 访问超出线程本地指针数组静态边界的线程本地指针;这可能会导致未定义的行为。 |
嵌入式平台
定义 BOOST_LEAF_EMBEDDED
等效于以下内容
#ifndef BOOST_LEAF_CFG_DIAGNOSTICS
# define BOOST_LEAF_CFG_DIAGNOSTICS 0
#endif
#ifndef BOOST_LEAF_CFG_STD_SYSTEM_ERROR
# define BOOST_LEAF_CFG_STD_SYSTEM_ERROR 0
#endif
#ifndef BOOST_LEAF_CFG_STD_STRING
# define BOOST_LEAF_CFG_STD_STRING 0
#endif
#ifndef BOOST_LEAF_CFG_CAPTURE
# define BOOST_LEAF_CFG_CAPTURE 0
#endif
LEAF 开箱即用地支持 FreeRTOS,请定义 BOOST_LEAF_TLS_FREERTOS
(在这种情况下,如果尚未定义,LEAF 会自动定义 BOOST_LEAF_EMBEDDED
)。
对于其他嵌入式平台,请定义 BOOST_LEAF_USE_TLS_ARRAY
,请参阅 配置 TLS 访问。
如果您的程序根本不使用并发,只需定义 BOOST_LEAF_NO_THREADS
,这不需要任何 TLS 支持(但 不是 线程安全的)。
与流行的看法相反,异常处理在嵌入式平台上运行良好。在 此演讲 中,Khalil Estell 演示了使用异常处理错误可以显着减少固件代码大小(当然,LEAF 可以在有或没有异常处理的情况下工作)。 |
可移植性
源代码与 C++11 或更高版本兼容。
LEAF 使用线程本地存储(仅用于指针)。默认情况下,这是通过 C++11 thread_local
存储类说明符实现的,但该库可以轻松配置为使用任何平台特定的 TLS API(它附带了对 FreeRTOS 的内置支持)。请参阅 配置。
运行单元测试
单元测试可以使用 Meson Build 或 Boost Build 运行。要运行单元测试
Meson 构建
将 LEAF 克隆到任何本地目录并执行
cd leaf
meson bld/debug
cd bld/debug
meson test
有关可用的构建选项,请参阅根目录中的 meson_options.txt
。
Boost 构建
假设当前工作目录是 <boostroot>/libs/leaf
../../b2 test
设计原理
- 定义
-
携带有关错误条件的信息的对象称为错误对象。例如,类型为
std::error_code
的对象是错误对象。
以下推理独立于用于传输错误对象的机制,无论是异常处理还是其他任何机制。 |
- 定义
-
根据函数与错误对象的交互方式,可以将函数分类如下
-
错误发起:通过创建新的错误对象来发起错误条件的函数。
-
错误中立:将较低级别函数调用的错误对象转发给调用方的函数。
-
错误处理:处理已接收的错误对象,恢复正常程序运行的函数。
-
一个关键的观察是,错误发起 函数通常是低级别函数,它们缺乏任何上下文,无法确定,更不用说指示,针对它们可能发起的错误采取正确的程序行为。在某些程序中(正确地)导致终止的错误条件可能在其他程序中(正确地)被忽略;而另一些程序可能会从中恢复并恢复正常运行。
同样的推理也适用于 错误中立 函数,但在这种情况下,还存在一个额外的问题,即它们需要传递的错误通常是由调用链中多个级别之外的函数发起的,这些函数通常是 — 并且应该被视为 — 实现细节。错误中立 函数不应与 错误发起 函数传递的错误对象类型耦合,原因与它不应与它们接口的任何其他方面耦合的原因相同。
最后,错误处理 函数,根据定义,具有处理至少某些(如果不是全部)故障所需的完整上下文。在它们的范围内,绝对必要的是,作者确切地知道较低级别函数必须传递哪些信息才能从每个错误条件中恢复。具体而言,所有这些必要信息都不能被视为实现细节;在这种情况下,在 错误中立 函数中要避免的耦合实际上是可取的。
我们现在准备定义我们的
- 设计目标
-
-
错误发起 函数应能够传递 所有 可用于它们的、与正在报告的故障相关的信息。
-
错误中立 函数不应与较低级别 错误发起 函数传递的错误类型耦合。它们应该能够使用可用于它们的其他相关信息来增强任何故障。
-
错误处理 函数应能够访问 错误发起 或 错误中立 函数传递的所有信息,这些信息是处理故障所需要的。
-
错误中立 函数不与通过它们的错误对象的静态类型耦合的设计目标似乎需要动态多态性和动态内存分配(Boost Exception 库以动态内存分配为代价满足此设计目标)。
事实证明,动态内存分配是不必要的,因为以下
- 事实
-
-
错误处理 函数“知道”错误发起 和 错误中立 函数 能够 传递的信息中,哪些是 实际需要的,以便处理特定程序中的故障。理想情况下,不应
使用浪费资源来存储或传递当前不需要用于处理错误的信息,即使它与故障相关。
-
例如,如果库函数能够传递错误代码,但程序不需要知道确切的错误代码,那么在库函数尝试传递它时,可以忽略该信息。另一方面,如果 错误处理 函数需要该信息,则存储它所需的内存可以静态地在其范围内保留。
LEAF 函数 try_handle_some
、try_handle_all
和 try_catch
实现了这个想法。用户提供错误处理 lambda 函数,每个函数都接受从特定错误条件恢复所需的类型的参数。LEAF 只是提供了存储这些类型所需的空间(以 std::tuple
的形式,使用自动存储持续时间),直到它们被传递给合适的处理程序。
在错误处理函数的范围内保留此空间时,所需错误类型的 thread_local
指针将设置为指向其中的相应对象。稍后,想要传递给定类型 E
的错误对象的 错误发起 或 错误中立 函数将使用相应的 thread_local
指针来检测当前是否为此类型提供了存储空间
-
如果指针不为空,则表示存储空间可用,并且对象将移动到指向的存储空间中,恰好一次 — 无论在到达 错误处理 函数之前必须展开多少级别的函数调用。
-
如果指针为空,则表示存储空间不可用,并且错误对象将被丢弃,因为在此程序中没有任何错误处理函数使用它 — 从而节省资源。
这几乎可以工作,但我们需要确保 错误处理 函数受到保护,免于访问响应先前故障而存储的陈旧错误对象,这将是一个严重的逻辑错误。为此,每次错误发生都会分配一个唯一的 error_id
。存储在错误处理范围内的每个 E…
对象也被分配一个 error_id
,将其永久地与特定的故障关联起来。
因此,要处理故障,我们只需将可用的错误对象(与其唯一的 error_id
相关联)与每个用户提供的错误处理函数所需的参数类型进行匹配。在 C++ 异常处理方面,就好像我们可以编写类似以下内容:
try
{
auto r = process_file();
//Success, use r:
....
}
catch(file_read_error &, e_file_name const & fn, e_errno const & err)
{
std::cerr <<
"Could not read " << fn << ", errno=" << err << std::endl;
}
catch(file_read_error &, e_errno const & err)
{
std::cerr <<
"File read error, errno=" << err << std::endl;
}
catch(file_read_error &)
{
std::cerr << "File read error!" << std::endl;
}
当然,此语法无效,因此 LEAF 使用 lambda 函数来表达相同的想法
leaf::try_catch(
[]
{
auto r = process_file(); //Throws in case of failure, error objects stored inside the try_catch scope
//Success, use r:
....
}
[](file_read_error &, e_file_name const & fn, e_errno const & err)
{
std::cerr <<
"Could not read " << fn << ", errno=" << err << std::endl;
},
[](file_read_error &, e_errno const & err)
{
std::cerr <<
"File read error, errno=" << err << std::endl;
},
[](file_read_error &)
{
std::cerr << "File read error!" << std::endl;
} );
类似的语法也适用于没有异常处理的情况。以下是使用
编写的相同代码片段result
<T>
return leaf::try_handle_some(
[]() -> leaf::result<void>
{
BOOST_LEAF_AUTO(r, process_file()); //In case of errors, error objects are stored inside the try_handle_some scope
//Success, use r:
....
return { };
}
[](leaf::match<error_enum, file_read_error>, e_file_name const & fn, e_errno const & err)
{
std::cerr <<
"Could not read " << fn << ", errno=" << err << std::endl;
},
[](leaf::match<error_enum, file_read_error>, e_errno const & err)
{
std::cerr <<
"File read error, errno=" << err << std::endl;
},
[](leaf::match<error_enum, file_read_error>)
{
std::cerr << "File read error!" << std::endl;
} );
result
| try_handle_some
| match
| e_file_name
| e_errno
局限性
使用动态链接时,要求错误类型以 default
可见性声明,例如
struct __attribute__ ((visibility ("default"))) my_error_info
{
int value;
};
除了在 Windows 上,这都可以按预期工作,在 Windows 上,线程本地存储在各个二进制模块之间不共享。因此,要在 DLL 边界之间传输错误对象。
使用动态链接时,始终最好使用 C 定义模块接口(并在适当的情况下使用 C++ 实现它们)。 |
LEAF 的替代方案
下面我们提供了 Boost LEAF 与 Boost Exception 和 Boost Outcome 的比较。
与 Boost 异常的比较
虽然 LEAF 可以在不使用异常处理的情况下使用,但在通过抛出异常来传递错误用例中,它可以被视为 Boost Exception 的更好、更高效的替代方案。LEAF 比 Boost Exception 具有以下优势
-
LEAF 不会动态分配内存;
-
LEAF 不会浪费系统资源来传递特定错误处理函数未使用的错误对象;
-
LEAF 不会将错误对象存储在异常对象中,因此它能够增强外部库抛出的异常(Boost Exception 只能增强派生自
boost::exception
的类型的异常)。
下表概述了当使用 Boost Exception 的代码重构为使用 LEAF 时应考虑的两个库之间的差异。
可以使用 LEAF 错误处理接口访问 Boost Exception 错误信息。请参阅 Boost Exception 集成。 |
Boost Exception | LEAF |
---|---|
|
|
Boost Exception | LEAF |
---|---|
|
|
Boost Exception | LEAF |
---|---|
|
|
Boost Exception | LEAF |
---|---|
|
|
Boost Exception | LEAF |
---|---|
所有提供的 |
用户定义的错误对象静态存储在 |
Boost Exception | LEAF |
---|---|
|
跨线程边界传输错误对象需要使用 [capture]。 |
Boost Exception | LEAF |
---|---|
|
LEAF 不使用 |
Boost Exception 存储所有提供的 如果用户需要完整的诊断消息,解决方案是使用 |
与 Boost Outcome 的比较
设计差异
与 LEAF 类似,Boost Outcome 库旨在在低延迟环境中使用。它提供了两个类模板,result<>
和 outcome<>
-
result<T,EC,NVP>
可以用作可能失败的noexcept
函数中的返回类型,其中T
指定成功情况下的返回值类型,而EC
是“错误代码”类型。从语义上讲,result<T,EC>
类似于std::variant<T,EC>
。当然,EC
默认为std::error_code
。 -
outcome<T,EC,EP,NVP>
类似于result<>
,但在失败的情况下,除了“错误代码”类型EC
之外,它还可以保存类型为EP
的“指针”对象,默认为std::exception_ptr
。
NVP 是一种策略类型,用于自定义当 result<> 或 outcome<> 对象包含错误时 .value() 的行为。 |
其思想是使用 result<>
来传递可以用“错误代码”完全指定的故障,并使用 outcome<>
来传递需要附加信息的故障。
描述此设计的另一种方法是,当返回某些静态类型 EC
的错误对象就足够时,使用 result<>
,而 outcome<>
还可以使用指针类型 EP
传输多态错误对象。
在 outcome<T> 的默认配置中,附加信息 — 或附加的多态对象 — 是由 std::exception_ptr 持有的异常对象。这针对的是当较低级别库函数抛出的异常需要通过一些非异常安全的中间上下文传输到能够处理它的更高级别上下文时的用例。LEAF 也直接支持此用例,请参阅 exception_to_result 。 |
类似的原因也推动了 LEAF 的设计。不同之处在于,虽然这两个库都认识到除了“错误代码”之外还需要传输“其他东西”,但 LEAF 为此问题提供了高效的解决方案,而 Outcome 将此负担转移给了用户。
leaf::result<>
模板删除了 EC
和 EP
,这使其与在失败情况下传输的错误对象的类型解耦。这使较低级别的函数可以自由地传递它们“知道”的关于故障的任何和所有信息:错误代码,甚至多个错误代码、文件名、URL、端口号等。同时,更高级别的错误处理函数控制特定客户端程序中需要哪些信息,哪些信息不需要。这是理想的,因为
-
较低级别库函数的作者缺乏上下文来确定哪些既与错误相关 又 自然可用于它们的信息需要传递,以便特定客户端程序可以从该错误中恢复;
-
更高级别错误处理函数的作者可以轻松且自信地做出此判断,并通过简单地编写不同的错误处理程序,自然地将其传达给 LEAF。LEAF 将传输所需的错误对象,同时丢弃处理程序不关心的对象,从而节省资源。
LEAF 示例包括对 Boost Outcome result<> 教程中程序的改编。您可以在 GitHub 上查看它。 |
使用 LEAF 进行错误处理的程序不需要使用 leaf::result<T> ;例如,可以将 outcome::result<T> 与 LEAF 一起使用。 |
互操作性问题
Boost Outcome 文档讨论了一个重要问题,即将多个库(每个库都使用自己的错误报告机制)整合在一起,并将它们纳入客户端程序中健壮的错误处理基础设施中。
建议用户尽可能在整个代码库中使用通用的错误处理系统,但由于这不切实际,result<>
和 outcome<>
模板都可以携带用户定义的“payload”。
以下分析来自 Boost Outcome 文档
如果库 A 使用 result<T, libraryA::failure_info>
,库 B 使用 result<T, libraryB::error_info>
,依此类推,那么对于应用程序编写者来说就出现了一个问题,他们引入了这些第三方依赖项并将它们绑定到一个应用程序中。作为一般规则,每个第三方库作者都不会为未知的其他第三方库构建显式的互操作支持。因此,问题落到了应用程序编写者身上。
应用程序编写者有三种选择
-
在应用程序中,使用的 result 形式是
result<T, std::variant<E1, E2, …>>
,其中E1, E2 …
是应用程序中使用的每个第三方库的失败类型。这样做的好处是完全保留了原始信息,但带来了一定的使用不便,并且可能导致高层和实现细节之间过度耦合。 -
可以在失败退出第三方库并进入应用程序的点,将第三方的失败类型翻译/映射到应用程序的失败类型。例如,可以使用 C 预处理器宏来包装应用程序对第三方 API 的每次调用。如果两个系统之间的映射不是一对一的,则此方法可能会丢失原始失败的详细信息,或者在某些情况下错误映射。
-
可以将第三方的失败类型类型擦除为某种应用程序失败类型,如果需要,以后可以重新构建。这是最干净的解决方案,耦合问题最少,并且没有错误映射的问题,但这几乎肯定需要使用
malloc
,而前两种方法不需要。
上述分析(强调部分是添加的)清晰而精确,但 LEAF 和 Boost Outcome 以不同的方式解决了互操作性问题
此外,考虑到 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 使用 这些自定义设置 渲染。