std::expectedBoost::Leaf 使用笔记

一直没有时间认真看一下Boost::Leaf。抽着有时间,把它和expect一起对比以下,看看它们有什么关系。

背景

Boost::Leafstd::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::LeafResult<T>, 好像它的意思是在result<T>中只提供期望值,非期望值不提供了。而是通过它使用的特殊的语法来获得。

std::expected接口

#include <expected>

C++23之前可以使用tl::expcettl_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):

修改

  • emplace: 原地构建
  • swap

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;	// 此时e1有默认值0, 有值
tl::expect<int,int> e2 = tl::make_unexpected(-1); // 设置错误值-1
tl::expect<int,int> e3{tl::unexpect, -1}; // 设置错误值-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()); // Bail out on error
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 )
.... // Handle err1::e1
else
.... // Handle any other err1 value
} );

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);
},
// 使用match, 匹配e1和e3
[]( 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
} );

在上面的例子中,第二个lambda会在err1的值是err1::e1err1::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();
} );

问题:上面的分支中,eei是什么关系?

使用leaf::try_handle_all()

try_hanle_all()会在编译期强制检查错误处理程序中至少有一个不接受参数,从而它能够处理任何失败。另外,所有的错误处理程序都必须返回一个有效的U类型,而不是result<U>,这样,函数返回的值类型是U而不是result<U>了。

这是try_handle_alltry_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
{
// Handle err::e1
},

[]( err1 e ) -> U
{
// Handle any other err1 value
},
// 必须要有这个,匹配所有未处理的错误!
[]() -> U
{
// Handle any other failure
} );

处理多个错误数据类别

如果流程中的函数返回不同的错误类型也是支持的,在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>
{
// Handle errors of type `err1`.
},

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

在上面的例子中,f1()返回错误类型err1f2()返回错误类型err2。一样可以处理,两个错误处理的lambda函数分别以err1err2类型为参数。如果错误和它们都不匹配,那么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>
{
// 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.
} );

在上面这个例子中,第一个错误处理函数会处理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 )
.... // Handle I/O errors when a file name is also available.
else
.... // Handle I/O errors when no file name is available.
} );

这里利用了leaf的一个特性:错误处理程序永远不会因为缺少作为指针的错误对象而被丢弃,此时会传递nullptr。在上面的错误处理中,当正常处理流程发送的错误对象值中只有io_error时,错误处理函数会把fn作为nullptr来调用。

也就是,大概是类似于这样:

1
[](io_error ec, e_file_name const * fu = nullptr)->leaf::result<U>...{}

增加错误

假设我们有一个函数parse_line(),它会产生两种错误:io_errorparse_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));

// use v
}
}

它的错误处理为:

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

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

// Produce and return a T.
}
// 使用异常实现:
T f()
{
if( error_detected )
leaf::throw_exception(err1::e1, err2::e2);

// Produce and return a T.
}

注意,在使用异常的版本中,使用了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 ); // 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
}
}

使用外部结果类型

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>
{
// Handle lib1::error_code
},

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

上面是理想情况,所有的函数都返回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() // Note: return type is not leaf::result<int>
{
auto a = foo();
if( !a )
return leaf::new_error( a.error() );

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

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

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

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>
{
// Handle lib1::error_code
},

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