std::expected以及其开源实现

std::expected以及其开源实现

  • 背景
  • std::expected和std::optional的区别
  • std::expected的接口介绍
    • 基本接口
    • Monad接口
  • std::expected的使用场景
  • std::expected的开源实现
    • tl::expected基本接口
    • tl::expected的Monad接口
    • 使用tl::expected进行函数式编程
    • tl::expected的内部实现
  • 总结

背景

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:std::optional类型实例持有T对象或者没有。它持有T对象时内部存储的是T对象本身而不是指向T的指针。类似语句std::optional a; 默认构造情况下,a不持有T对象。
  • std::variant: std::variant类型的对象表示某一时刻该对象可能是T1, T2, T3中的任一类型或者不持有任何对象,换言之,std::variant是一个类型安全的union
  • std::any: 一个std::any在某一时刻可以表示任意类型的对象

std::expected正常情况下表示T,非正常情况下表示E。同一时刻只能表示一个值, 要么表示正常值, 要么表示非正常的值。

std::expected和std::optional的区别

std::optionalstd::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的接口介绍

std::expected接口里面的T和E的类型必须满足下面两点:

  • T 是expected value的类型,可以带cv,也可以是void,但必须是可析构的(不支持数组或者引用类型)
  • E 是unexpected value的类型,必须是可析构的,E的类型必须支持std::unexpected的模板参数类型(不可以带cv, 也不可以是void,不支持数组或者引用类型)

基本接口

  1. operator bool has_value() 判断是否含有expected value
  2. const T& value() 获取expected value,返回的是const 引用, 如果expected value不存在, 抛std::bad_expected_access异常。
  3. const E& error() 获取unexpected value,返回的是const 引用,如果unexpected value不存在(即has_value返回true), 调用error()将会产生undefined行为。
  4. 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接口

标准也定义了一些monad接口,使得使用std::expected对象时,可以很容易的使用Functional Programming的风格:

  1. and_then:如果含有expected value,Monad函数作用于expected value, 返回新的std::expected对象;如果不含有expected value,返回原来的std::expected对象拷贝。
  2. transform: 和and_then类似,但是Monad函数作用于原来的expected value,返回原来的std::expected对象;如果不含有expected value,返回原来的std::expected对象。
  3. or_else:如果含有unexpected value,Monad函数作用于unexpected value, 返回新的std::expected对象;如果不含有unexpected value,返回原来的std::expected对象拷贝。
  4. transform_error: 和or_else类似,但是Monad函数作用于原来的unexpected value,返回原来的std::expected对象;如果不含有unexpected value,返回原来的std::expected对象。

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的开源实现

由于标准中的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基本接口

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

tl::expected的Monad接口

通过下面的例子,其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可以用于函数式编程和非函数式编程风格的代码。

对于非函数式风格编程,下面是示例代码:

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的内部实现

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

  1. https://en.cppreference.com/w/cpp/utility/expected
  2. https://github.com/TartanLlama/expected

你可能感兴趣的:(C++,c++)