std::expected 是C++23 中增加的一个新特性,它的作用是提供一种机制,使得一个对象中只可能是一个正常情况下期望的值或者非正常情况下返回错误信息。它特别适合作为函数的返回值类型。
The class template std::expected provides a way to store either of two values. An object of std::expected at any given time either holds an expected value of type T, or an unexpected value of type E. std::expected is never valueless.
C++17中std::optional, std::variant, std::any等都可以表示多个值,但它们不完全相同:
std::optional
类型实例持有T对象或者没有。它持有T对象时内部存储的是T对象本身而不是指向T的指针。类似语句std::optional a;
默认构造情况下,a
不持有T对象。std::variant
类型的对象表示某一时刻该对象可能是T1, T2, T3中的任一类型或者不持有任何对象,换言之,std::variant
是一个类型安全的union
std::any
在某一时刻可以表示任意类型的对象std::expected
正常情况下表示T,非正常情况下表示E。同一时刻只能表示一个值, 要么表示正常值, 要么表示非正常的值。
std::optional
和std::expected
比较相似,std::optional
表示T的值可能存在,也可能不存在,在std::optional之前,我们可能用一些临界值或NULL表示T在异常情况下的取值,
// declare
int getItmeType(int item_id) {
if (item_id < 0) {
return INT_MAX;
}
int type = item_id;
return type;
}
// usage
int type = getItemType(id);
if (type == INT_MAX) {
LOG("Invalid item type");
}
对于采用std::optional
的情况:
// declare
std::optional<int> getItmeType(int item_id) {
if (item_id < 0) {
return std::nullopt;
}
int type = item_id;
return type;
}
// usage
std::optional<int> type = getItemType(id);
if (!type) {
LOG("Invalid item type");
}
对于采用std::expected
, 我们可以类似这样实现:
// declare
enum ITEM_ID_ERROR_CASE {
ID_TOO_BIG,
ID_TOO_SMALL,
};
std::expected<int, ITEM_ID_ERROR_CASE > getItmeType(int item_id) {
if (item_id < 0) {
return std::unexpected<ID_TOO_SMALL>();
}
if (item_id > 10000) {
return std::unexpected<ID_TOO_BIG>();
}
int type = item_id;
return type;
}
// usage
std::expected<int, ITEM_ID_ERROR_CASE > type = getItemType(id);
if (!type.has_value()) {
switch(type.error()) {
case ID_TOO_SMALL: {
LOG("item id too small");
break;
}
case ID_TOO_BIG: {
LOG("item id too big");
break;
}
}
return;
}
int real_type = type.value();
std::expected
接口里面的T和E的类型必须满足下面两点:
operator bool has_value()
判断是否含有expected valueconst T& value()
获取expected value,返回的是const 引用, 如果expected value不存在, 抛std::bad_expected_access
异常。const E& error()
获取unexpected value,返回的是const 引用,如果unexpected value不存在(即has_value返回true), 调用error()将会产生undefined行为。const T& value_or(U&& default_value)
获取expected value, 如果expected value不存在,返回default_value持有一个std::expected对象,一般的使用流程是通过has_value来判断是否含有expected value,再通过接口2或者3来获取对应的T或者E的值,注意这里必须通过接口1事先判断是否有expected value,如果没有还强行获取,就会抛出异常。
接口4使用的时候不需要通过接口1进行事先确认。
标准也定义了一些monad接口,使得使用std::expected
对象时,可以很容易的使用Functional Programming的风格:
and_then
:如果含有expected value,Monad函数作用于expected value, 返回新的std::expected对象;如果不含有expected value,返回原来的std::expected对象拷贝。transform
: 和and_then
类似,但是Monad函数作用于原来的expected value,返回原来的std::expected对象;如果不含有expected value,返回原来的std::expected对象。or_else
:如果含有unexpected value,Monad函数作用于unexpected value, 返回新的std::expected对象;如果不含有unexpected value,返回原来的std::expected对象拷贝。transform_error
: 和or_else
类似,但是Monad函数作用于原来的unexpected value,返回原来的std::expected对象;如果不含有unexpected value,返回原来的std::expected对象。很多情况下,我们在写一个函数时,有时除了需要知道函数执行是否成功外,还需要在执行错误的情况下,知道出错信息或者错误码,错误码的例子在上面已经给出了,对于出错信息多数情况下可能会这样实现:
// declare
std::pair<bool, std::string> doSomething() {
if (condition_1) {
return std::make_pair<false, "condition 1 check failed.">;
}
if (condition_2) {
return std::make_pair<false, "condition 2 check failed.">;
}
return std::make_pair<true, "">;
}
//usage
auto ret = doSomething();
if (!ret.first) {
LOG(ret.second.c_str());
return;
}
在C++23中,如果用std::expected, 上面的实现可以简单的替换为:
// declare
std::expected<bool, std::string> doSomething() {
if (condition_1) {
return std::unexpected<"condition 1 check failed.">;
}
if (condition_2) {
return std::unexpected<"condition 2 check failed.">;
}
return true;
}
//usage
auto ret = doSomething();
if (!ret.has_value()) {
LOG(ret.error().c_str());
return;
}
由于标准中的std::expected
需要使用C++23,加上各个编译器组织实现标准的进度,要想在现在的项目中使用std::expected
在短期内基本是不可能的。
tl::expected 是std::expected
的一个开源实现,它支持C++11/14/17,和标准的接口非常类似:其基本接口和std::expected
完全一致,其Monad接口在命名方式上稍有区别:transform
接口命名为map
接口,transform_error
接口命名为map_error
接口。
良好的测试用例就是代码最好的文档,tl::expected拥有非常清晰的测试用例,下面通过其测试用例代码,来介绍其使用:
tl::expected
重载了operator()
, 因而可以不用显示的调用has_value()
来判断是否存有expected value:
constexpr bool has_value() const noexcept { return this->m_has_val; }
constexpr explicit operator bool() const noexcept { return this->m_has_val; }
其基本的构造方法如下:
{
tl::expected<int,int> e;
REQUIRE(e);
REQUIRE(e.has_value());
REQUIRE(e == 0);
}
{
tl::expected<int,int> e = tl::make_unexpected(0); // same as std::unexpected(0)
REQUIRE(!e);
REQUIRE(!e.has_value());
REQUIRE(e.error() == 0);
REQUIRE(e.value_or(5) == 5);
}
{
tl::expected<int,int> e (tl::unexpect, 0);
REQUIRE(!e);
REQUIRE(e.error() == 0);
}
{
tl::expected<int,int> e (tl::in_place, 42);
REQUIRE(e);
REQUIRE(e == 42);
REQUIRE(e.value() == 42);
REQUIRE(*e == 42);
}
{
tl::expected<std::vector<int>,int> e (tl::in_place, {0,1});
REQUIRE(e);
REQUIRE((*e)[0] == 0);
REQUIRE((*e)[1] == 1);
}
{
tl::expected<std::tuple<int,int>,int> e (tl::in_place, 0, 1);
REQUIRE(e);
REQUIRE(std::get<0>(*e) == 0);
REQUIRE(std::get<1>(*e) == 1);
}
通过下面的例子,其monad接口可以很容易的理解:
auto mul2 = [](int a) { return a * 2; };
auto ret_void = [](int a) { (void)a; };
// map
{
tl::expected<int, int> e = 21;
auto ret = e.map(mul2);
REQUIRE(ret);
REQUIRE(ret == 42);
REQUIRE(*ret == 42);
}
{
tl::expected<int, int> e = 21;
auto ret = e.map(ret_void);
REQUIRE(ret);
STATIC_REQUIRE(
(std::is_same<decltype(ret), tl::expected<void, int>>::value));
}
// map error
{
tl::expected<int, int> e = 21;
auto ret = e.map_error(mul2);
REQUIRE(ret);
REQUIRE(*ret == 21);
}
{
tl::expected<int, int> e(tl::unexpect, 21);
auto ret = e.map_error(mul2);
REQUIRE(!ret);
REQUIRE(ret.error() == 42);
}
// and_then
auto succeed = [](int a) { (void)a; return tl::expected<int, int>(21 * 2); };
auto fail = [](int a) { (void)a; return tl::expected<int, int>(tl::unexpect, 17); };
{
tl::expected<int, int> e = 21;
auto ret = e.and_then(succeed);
REQUIRE(ret);
REQUIRE(*ret == 42);
REQUIRE(e);
REQUIRE(*e == 21);
}
{
tl::expected<int, int> e = 21;
auto ret = std::move(e).and_then(fail);
REQUIRE(!ret);
REQUIRE(ret.error() == 17);
}
{
const tl::expected<int, int> e(tl::unexpect, 21);
auto ret = e.and_then(succeed);
REQUIRE(!ret);
REQUIRE(ret.error() == 21);
}
{
tl::expected<int, int> e(tl::unexpect, 21);
auto ret = e.and_then(fail);
REQUIRE(!ret);
REQUIRE(ret.error() == 21);
}
// or_else
{
tl::expected<int, int> e = 21;
const auto& ret = e.or_else(succeed);
REQUIRE(ret);
REQUIRE(*ret == 21);
}
{
tl::expected<int, int> e(tl::unexpect, 21);
auto ret = e.or_else(succeed);
REQUIRE(ret);
REQUIRE(*ret == 42);
}
{
tl::expected<int, int> e(tl::unexpect, 21);
auto ret = e.or_else(fail);
REQUIRE(!ret);
REQUIRE(ret.error() == 17);
}
这里以tl::expected中的样例进行介绍。tl::expected可以用于函数式编程和非函数式编程风格的代码。
对于非函数式风格编程,下面是示例代码:
tl::expected<image,fail_reason> get_cute_cat (const image& img) {
auto cropped = crop_to_cat(img);
if (!cropped) {
return cropped;
}
auto with_tie = add_bow_tie(*cropped);
if (!with_tie) {
return with_tie;
}
auto with_sparkles = make_eyes_sparkle(*with_tie);
if (!with_sparkles) {
return with_sparkles;
}
return add_rainbow(make_smaller(*with_sparkles));
}
使用其函数式编程接口,上面的例子可以改写为如下函数式编程的代码:
tl::expected<image,fail_reason> get_cute_cat (const image& img) {
return crop_to_cat(img)
.and_then(add_bow_tie)
.and_then(make_eyes_sparkle)
.map(make_smaller)
.map(add_rainbow);
}
crop_to_cat
返回一个tl::expected
对象, 如果返回的是expected value存在值,则直接调用add_bow_tie
,否则返回…, 总的逻辑上和上面的例子一样,但是代码可以写的更精简和优雅。
tl::expected的内部实现如下,它通过一个union和一个flag来实现:通过union存储T或者E,通过flag来判断当前是T还是E。
union {
T m_val;
unexpected<E> m_unexpect;
char m_no_init;
};
bool m_has_val;
tl::expected内部采用union的形式,实现了一种非常好用的函数值返回类型。同时,通过提供monad接口,使得使用std::expeted对象时,可以很方便的使用类似Functional Programming的风格进行编程。
Reference