std::expected
和Boost::Leaf
使用笔记一直没有时间认真看一下Boost::Leaf
。抽着有时间,把它和expect
一起对比以下,看看它们有什么关系。
背景 Boost::Leaf
和std::expected
, std::optional
, std::variant
等的区别:
optional
是C++17提供的, expected
是C++23提供的
std::optional<T>
持有对象T或没有. 当它持有对象T时, 存储的是T对象本身而不是指针.
std::variant<T1,T2,T3>
, 表示的是某一时刻该对象可能是T1, T2, T3种的任意一个( 有没有可能不持有任何对象? 要再去看看)
std::any
表示可以表示任意类型的对象
std::expected<T,E>
正常情况下表示T, 非正常情况下表示E, 同一时刻只能表示一个值
Boost::Leaf
的Result<T>
, 好像它的意思是在result<T>
中只提供期望值,非期望值不提供了。而是通过它使用的特殊的语法来获得。
std::expected
接口#include <expected>
C++23之前可以使用tl::expcet
:tl_expect github: https://github.com/TartanLlama/expected.git
基本接口 基本接口和optional
有点类似:
operator bool has_value()
:
const T& value()
: 可能会抛出异常std::bad_expected_access
的
const E& error()
: 在has_value
时被调用, 行为未定义
const T& value_or(U&& default_value)
:
修改
Monadic接口
and_then
: 如果含有期望值T, 则函数F作用于T, 并返回新的expected
值(T的). 如果没有, 返回原来的expected
值得拷贝:
template< class F > constexpr auto and_then( F&& f ) &;
等
returns the result of the given function on the expected value if it exists; otherwise, returns the expected
itself
transform
: 如果有期望值, 函数F作用于其上, 返回原来得expected
对象; 如果没有期望值, 返回原来得expected
对象
在tl::expect
中,名字为map
returns an expected
containing the transformed expected value if it exists; otherwise, returns the expected
itself
or_else
: 如果含有错误值E(意思是出错了), 则Monad函数作用于E上, 返回新得expected
对象, 如果不含有期望值
returns the expected
itself if it contains an expected value; otherwise, returns the result of the given function on the unexpected value
transform_error
: returns the expected
itself if it contains an expected value; otherwise, returns an expected
containing the transformed unexpected value
在tl::expcect
中, 名字为map_error
构造tl::expect
1 2 3 4 5 tl::expect<int ,int > e1; tl::expect<int ,int > e2 = tl::make_unexpected (-1 ); tl::expect<int ,int > e3{tl::unexpect, -1 }; tl::expect<std::pair<int ,int >, int > e4{tl::in_place, {1 ,2 }}; tl::expect<std::vector<int >, int > e5{tl::in_place, {1 ,2 ,3 }};
使用Monad接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 auto mul2 = [](int a){ return a*2 ; };auto ret_void = [](int a){ (void )a; };{ tl::expected<int , int > e{21 }; auto ret = e.map (mul2); qDebug () << TSHOW ((ret==42 )) << TSHOW ((*ret==42 )); if (ret) { qDebug () << "ret has value" ; } } auto succeed_s = [](int a){ return tl::expected <QString, int >(QString::number (a,16 )); };auto succeed_a = [](QString a){ return tl::expected <QString,int >(a.toUpper ());};{ tl::expected<int ,int > e{21 }; auto ret = e.and_then (succeed_s).and_then (succeed_a); qDebug () << ret.value (); }
注意, and_then
等函数的输入类型都应该是expect
内部封装的类型, 而输出也应该是tl::expect
.
函数式编程使用tl::expect
1 2 3 4 5 6 7 8 9 10 11 12 tl::expect<Image, Error> crop_to_cat (Image img) ;tl::expect<Image, Error> add_bow_tie (Image img) ;tl::expect<Image, Error> make_eyes_sparkle (Image img) ;tl::expect<Image, Error> make_smaller (Image img) ;tl::expect<Image, Error> get_cute_cat (const image& img) { return crop_to_cat (img) .and_then (add_bow_tie) .and_then (make_eyes_spakle) .map (make_smaller) ; }
Boost.Leaf 基本知识 Leaf
还是一个相对比较新的库,大概进入Boost没有几年(22年?)。
从它的解释,大概可以理解为,作者认为相对于正常的result<T,E>
,里面的E
并不是总是要用到的,因此,可以将其简化为result<T>
,而E
则会被传递到需要它们的错误处理作用域去处理。作者认为这样会让result<T>
变得非常轻量级(不懂),不与错误类型耦合(这个我没有意见),错误对象在常量时间内通信。总而言之是轻量化(这个倒是没有感受到)
Leaf
下面result
的表达方式和expect
倒是很类似。区别是不需要像expect
一样定义错误类别了。
1 2 3 4 5 6 7 8 9 10 enum class err1 {e1,e2,e3};boost::leaf::result<T> f () { ... if (error_detected) { return boost::leaf::new_error (err1::e1); } ... }
在上面这段代码中,错误值是通过leaf::new_error(...)
来创建的。对照着expect
,需要这样: tl::make_unexpect{-1}
或者{tl::unexpect, -1}
这种写法。
基本使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 leaf::result<QString> f (int v) { if (v<10 ){ return leaf::new_error (err1::e1); } return QString::number (v); } leaf::result<int > g (int val) { #if 1 auto r = f (val); if (!r){ return r.error (); } #else BOOST_LEAF_AUTO (r, f (val)); #endif return r->toInt (); } void main () { for (int i=5 ; i<15 ; ++i) { auto v = g (i); if (v) qDebug () << "value of " << i << "is: " << v.value (); else qDebug () << "error when i=" << i; } }
在上面,g()
中的判断r是否有效,可以换一个宏:BOOST_LEAF_AUTO
来写。就可以把语句if...
给省略掉。
如果f
返回值是void
,则使用BOOST_LEAF_CHECK
来写:
1 2 3 4 5 6 7 leaf::result<void > f () ;leaf::result<int > g () { BOOST_LEAF_CHECK (f ()); return 42 ; }
Leaf下的错误处理 使用leaf::try_handle_some() 下面是错误处理的一个例子:
leaf::try_handle_some
是一个函数,它的第一个参数是一个lambda,是要执行的逻辑,第二个参数也是一个lambda,处理错误:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 ) .... else .... } );
try_handle_some
也可以有多个lambda,每个处理一个或几个错误类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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> { }, []( err1 e ) -> leaf::result<U> { } );
在上面的例子中,第二个lambda会在err1
的值是err1::e1
或err1::e2
时被处理:使用leaf::match
方法做匹配。如果匹配补上,那么第三个lamba会被调用。
另一种写法,错误处理程序有条件的不处理失败:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 (); } );
问题 :上面的分支中,e
和ei
是什么关系?
使用leaf::try_handle_all()
try_hanle_all()
会在编译期强制检查错误处理程序中至少有一个不接受参数,从而它能够处理任何失败。另外,所有的错误处理程序都必须返回一个有效的U
类型,而不是result<U>
,这样,函数返回的值类型是U
而不是result<U>
了。
这是try_handle_all
和try_handle_some
最大的区别,它返回包装的值了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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 { }, []( err1 e ) -> U { }, []() -> U { } );
处理多个错误数据类别 如果流程中的函数返回不同的错误类型也是支持的,在try_handl_some()
中分别处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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> { }, []( err2 e ) -> leaf::result<U> { } );
在上面的例子中,f1()
返回错误类型err1
,f2()
返回错误类型err2
。一样可以处理,两个错误处理的lambda函数分别以err1
和err2
类型为参数。如果错误和它们都不匹配,那么try_hanle_some
会失败。
处理多个错误对象 可以用多个错误对象调用leaf::new_error()
函数。例如,下面的代码中,open_file()
失败时,会传递错误代码和文件名:
1 2 3 4 5 6 7 8 9 10 11 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}); .... }
在这个时候,它的错误处理函数则接收多个错误对象作为参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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_error ec ) -> leaf::result<U> { } );
在上面这个例子中,第一个错误处理函数会处理open_file()
返回的错误,它接收了两个参数
第二个错误处理函数会处理其他的错误,它只接受一个参数
其他情况下,try_handle_some()
会返回失败
另外一种写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 ) .... else .... } );
这里利用了leaf
的一个特性:错误处理程序永远不会因为缺少作为指针的错误对象而被丢弃,此时会传递nullptr
。在上面的错误处理中,当正常处理流程发送的错误对象值中只有io_error
时,错误处理函数会把fn
作为nullptr
来调用。
也就是,大概是类似于这样:
1 [](io_error ec, e_file_name const * fu = nullptr)->leaf::result<U>...{}
增加错误 假设我们有一个函数parse_line()
,它会产生两种错误:io_error
或parse_error
:
1 2 3 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 ) ;
下面的代码,我们先创建了一个错误对象load
,当parse_line()
发生错误时,load
会被自动附加到上面。如果没有错误,就会被抛弃。process_file()
并不处理错误,只是完成这个“附加”处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 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)); } }
它的错误处理为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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; } );
或者下面这种,它将e_line
作为指针参数传递,从而可以省略一个错误处理程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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()
都支持捕获异常,可以将异常捕获和其他的错误处理放在一起:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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; } );
如果你只使用异常,不使用result
,则可以使用leaf::try_catch
。此时就完全不使用返回错误的机制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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; } );
LEAF使用一种新的异常处理技术,它不需要异常类型层次结构对故障做分类,也不在异常对象中携带数据。
下面分别是使用result
技术和异常
实现的同一个函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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 f () { if ( error_detected ) leaf::throw_exception (err1::e1, err2::e2); }
注意,在使用异常的版本中,使用了leaf::throw_exception
,而不是C++的throw
。和传统的C++异常使用异常类型来区分不同异常不一样,在LEAF中,异常的类别并不重要。leaf::try_catch
会捕获所有异常,然后通过通常的leaf错误处理程序选择例程。
如果我们想使用抛出不同类型来指示不同失败的惯例,只需将一个异常对象(即派生自std::exception类型的对象)作为leaf::throw_exception的第一个参数:
1 leaf::throw_exception (std::runtime_error ("Error!" ), err1::e1, err2::e2);
我们重写process_file()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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); } }
使用外部结果类型 c++程序通常需要处理通过多层api(通过大量的错误代码、结果类型和异常)传递的错误。LEAF使应用程序开发人员能够将错误对象从每个库的结果类型中分离出来,并将它们逐字发送到错误处理作用域。
例如,下面的例子中,foo()
和bar()
分别是不同的库中的函数。我们在try_handle_some
中处理所有的错误:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 () ); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 leaf::result<int > r = leaf::try_handle_some ( []() -> leaf::result<int > { return f (); }, []( lib1::error_code ec ) -> leaf::result<int > { }, []( lib2::error_code ec ) -> leaf::result<int > { } ); }
上面是理想情况,所有的函数都返回result
。但是这种情况并不一定总能得到满足。有时我们可能无法从f
返回leaf::result<T>
的途径。比如,第三方API可能会对函数施加特定的签名,迫使其返回特定于库的结果类型。例如,当f
用作回调时:
1 void register_callback ( std::function<lib3::result<int >()> const & callback ) ;
对这种情况,只要libb3::result
能够传递std::error_code
。我们只需要让LEAF知道,通过专门化is_result_type
模板:
1 2 3 4 5 6 namespace boost { namespace leaf {template <class T >struct is_result_type <lib3::result<T>>: std::true_type;} }
这样,即使lib::result
不能传输lib1错误或lib2错误,也能像以前一样工作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 lib1::result<int , lib1::error_type> foo () ;lib2::result<int , lib2::error_type> bar () ;int g ( int a, int b ) ;lib3::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::new_error
返回的对象使用特定于leaf的error_category
隐式地转换为std::error_code
,这使得lib3::result
与leaf::try_handle_some
(以及leaf::try_handle_all
)兼容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 lib3::result<int > r = leaf::try_handle_some ( []() -> lib3::result<int > { return f (); }, []( lib1::error_code ec ) -> lib3::result<int > { }, []( lib2::error_code ec ) -> lib3::result<int > { } ); }