std::expected以及其开源实现

Posted ithiker

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了std::expected以及其开源实现相关的知识,希望对你有一定的参考价值。

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

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

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

std::optionalstd::expected比较相似,std::optional<T>表示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<T>的情况:

// 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和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的值。

Monad接口

标准也定义了一些monad接口,使得使用std::expected<T, E>对象时,可以很容易的使用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::pair<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::expectedstd::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内部采用union的形式,实现了一种非常好用的函数值返回类型。同时,通过提供monad接口,使的使用该对象时,可以很方便的使用类似Functional Programming的风格进行编程。


Reference

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

以上是关于std::expected以及其开源实现的主要内容,如果未能解决你的问题,请参考以下文章

开源项目葫芦藤:IdentityServer4的实现及其运用

AliSQL开源功能特性

研发干货丨基于OK3399-C平台android系统下实现图像识别

JAVA反射改动常量,以及其局限

介绍XXTEA加密算法及其C实现

基于 Hive 的文件格式:RCFile 简介及其应用