摘要
Boost.Leaf是一个给C++11搞的轻量级错误处理库。
- 单头文件,无依赖
- 嵌入式开发生成代码小
- 没有动态内存分配,即使错误信息很大
- 出没出错效率一样
- 处理错误时间恒定,与调用堆栈深度无关
- 可选异常机制
支持
获取
许可证:Boost Software License - Version 1.0
包含于:Boost 1.75
源码:GitHub
单个头文件:leaf.hpp
ℹ️LEAF不依赖Boost和其它库
教程
什么是一个失败呢?。。。
报告错误
enum class err1 { e1, e2, e3 };
leaf::result<T> f()
{
....
if( error_detected )
return leaf::new_error( err1::e1 ); // 传递任何类型的错误对象
// 返回T
}
检查错误
leaf::result<U> g()
{
leaf::result<T> r = f();
if( !r )
return r.error();
T const & v = r.value();
// 使用v产出U
}
💡
r.error()
兼容任何类型的leaf::result
if
语句可以通过BOOST_LEAF_AUTO
简写:
leaf::result<U> g()
{
BOOST_LEAF_AUTO(v, f()); // Bail out on error
// 使用v产出U
}
BOOST_LEAF_AUTO
不能用于void
结果,这种情况下我们使用BOOST_LEAF_CHECK
:
leaf::result<void> f();
leaf::result<int> g()
{
BOOST_LEAF_CHECK(f()); // 发出错误
return 42;
}
在支持__GNUC__
的编译器中,BOOST_LEAF_CHECK
可以利用Statement Exprs (Using the GNU Compiler Collection (GCC))的特性,使得非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 )
.... // 处理err1::e1错误
else
.... // 处理其他err1错误
} );
try_handle_some
第一个lambda参数首先执行,它试图创建结果U
,但它可能会失败。
第二个lambda参数是一个错误处理程序:如果第一个lambda执行失败并且产生err1
错误,则它被调用。错误对象被存储在栈上,在try_handle_some
函数中(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>
{
// 处理err::e1或err1::e3错误
},
[]( err1 e ) -> leaf::result<U>
{
// 处理其它err1错误
} );
LEAF依序对比错误处理程序,根据错误对象,调用第一个它可以提供参数的错误处理程序,上面的程序中:
- 第一个错误处理程序通过
leaf::match
指定仅当错误包含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
之外。
💡如果我们想发出一个新的错误(而不是传递当前错误),我们可以通过
leaf::new_error
返回新的错误。
如果我们想确保所有错误都被处理,我们可以使用leaf::try_handle_all
:
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
{
// 处理err::e1错误
},
[]( err1 e ) -> U
{
// 处理其他err1错误
},
[]() -> U
{
// 处理其它任何错误
} );
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>
{
// 处理err1类型错误
},
[]( err2 e ) -> leaf::result<U>
{
// 处理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>
{
// 处理IO错误,并且文件名可用
},
[]( io_error ec ) -> leaf::result<U>
{
// 处理IO错误,文件名不可用
} );
重复一次,错误处理程序被依次考虑:
- 当错误包含
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 )
.... // 处理IO错误,并且文件名可用
else
.... // 处理IO错误,文件名不可用
} );
错误处理程序不会因不包含指针类型指向的错误类型的对象而被跳过;此时,LEAF直接传递nullptr
。
💡错误处理程序可以接受值、(const)引用或(const)指针类型参数;使用引用或指针时,如果修改错误对象并且不处理错误,则该状态会被保留并向上传播。
增加错误
假如我们有函数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));
// 使用v
}
}
process_file
不处理错误,除非出现问题时附加current_line
, on_error
返回的对象包含一份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会正确分发错误,不论错误来自于leaf::result
还是来自于异常。
当然,如果我们显示使用异常处理,我们就不需要leaf::result
了,此时我们用leaf::try_catch
:
leaf::try_catch(
[]
{
process_file(f);
},
[]( std::bad_alloc const & )
{
std::cerr << "Out of memory!" << std::endl;
},
[]( parse_error e, e_line l )
{
std::cerr << "Parse error at line " << l.value << std::endl;
},
[]( io_error e, e_line const * l )
{
std::cerr << "Parse error";
if( l )
std::cerr << " at line " << l.value;
std::cerr << std::endl;
} );
注意,我们不需要修改错误处理程序!但这是咋工作的呢?process_file
抛出了啥?
LEAF使用了一个新的异常处理技术,它不使用异常类型继承关系来对错误进行分类,也不在异常中携带数据。回想一下,我们通过leaf::result
传递错误时,我们会调用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);
// 创建并返回T
}
使用异常处理时,我们这样:
enum class err1 { e1, e2, e3 };
enum class err2 { e1, e2 };
T f()
{
if( error_detected )
leaf::throw_exception(err1::e1, err2::e2);
// 创建并返回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
函数:
int parse_line( FILE * f ); // 抛出
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);
// 使用v
}
}
使用外部结果类型
TODO
Binding Error Handlers in a std::tuple
Transporting Error Objects Between Threads
Classification of Failures
Converting Exceptions to result
Using error_monitor to Report Arbitrary Errors from C-callbacks
Diagnostic Information
Working with std::error_code, std::error_condition
…