any optional variant

六月 20, 2022 [c++] #c++

any optional variant

any optional variant

std::optional, std::any, 和 std::variant 有一个共同特点:他们都支持就地构造.另外的,std::variant 还支持访问者模式.

首先,我们要了解一下这3种数据类型的功能作用.

std::optional 是一种可能包含也可能不包含某一类型对象的类型. std::variant 是一种类型安全的联合体 std::any 是一种可以包含任意类型(指可复制类型)对象的类型

Construct in-place

什么是就地构造呢?以 std::optional<std::string> 为例来说明就是: 所谓就地构造,就是你可以直接使用 std::string 的构造参数来构造 std::optional<std::string>.

std::optional<std::string> opt1(std::in_place, "C++17");

std::variant 支持 std::visit 方法(即精典的设计模式:访问者).

std::visit 方法允许你对一个 std::variants 列表应用访问者模式,而相应的访问者必须是一个callable类型,所谓 callable 类型,是一种可以被调用的类型,通常是一个函数,一个函数对象或者一个 lambda 函数.简单起见,这里我仅使用 lambda 函数来举例说明.

#include <iostream>
#include <vector>
#include <typeinfo>
#include <type_traits>
#include <variant>

int main()
{
    std::cout << std::endl;

    std::vector<std::variant<char, long, float, int, double, long long>>      // 1
        vecVariant = { 5, '2', 5.4, 100ll, 2011l, 3.5f, 2017 };

    // display each value                                                             
    for (auto& v : vecVariant) {
        std::visit([](auto&& arg) {std::cout << arg << " "; }, v);                // 2
    }
    std::cout << std::endl;

    // display each type
    for (auto& v : vecVariant) {
        std::visit([](auto&& arg) {std::cout << typeid(arg).name() << " "; }, v); // 3
    }
    std::cout << std::endl;

    // get the sum
    std::common_type<char, long, float, int, double, long long>::type res{};  // 4
    std::cout << "typeid(res).name(): " << typeid(res).name() << std::endl;

    for (auto& v : vecVariant) {
        std::visit([&res](auto&& arg) {res += arg; }, v);                          // 5
    }
    std::cout << "res: " << res << std::endl;

    // double each value
    for (auto& v : vecVariant) {
        std::visit([](auto&& arg) {arg *= 2; }, v);                           // 6
        std::visit([](auto&& arg) {std::cout << arg << " "; }, v);
    }
    std::cout << std::endl;

    return 0;
}

common_type函数

包含于<type_traits>当中。 作用是返回参数列表中的参数都可以转换成的类型(有些拗口)。

C++11中的用法为

syd::common_type<int,float,double>::type // 返回值为double 

std::any

一个类型安全的容器,可以放置各种类型的数据。

#include <any>
#include <iostream>

int main()
{
  std::cout << std::boolalpha; //将bool值用 "true" 和 "false"显示
  std::any a;  //定义一个空的any,即一个空的容器
  //有两种方法来判断一个any是否是空的
  std << cout << a.has_value() <<std::endl; // any是空的时,has_value 返回值为 false
  std << cout << a.type().name() <<std::endl; //any 是空的时,has_value 返回值为 true
  //几种创建any的方式
  std::any b = 1;                         //b 为存了int类型的值的any
  auto c = std::make_any<float>(5.0f);    //c为存了float类型的any
  std::any d(6.0);                        //d为存储了double类型的any
  std << cout << b.has_value() <<std::endl;   //true 
  std << cout << b.type().name() <<std::endl; //int
  std << cout << c.has_value() <<std::endl;   //true
  std << cout << c.type().name() <<std::endl; //float
  std << cout << d.has_value() <<std::endl;   //true
  std << cout << d.type().name() <<std::endl; //double
  
  //更改any的值    
  a = 2;   //直接重新赋值
  auto e = c.emplace<float>(4.0f); //调用emplace函数,e为新生成的对象引用

  //清空any的值
  b.reset();
  std << cout << b.has_value() <<std::endl;   //false
  std << cout << b.type().name() <<std::endl; //int

  //使用any的值
  try
  {
    auto f = std::any_cast<int>(a); //f为int类型,其值为2
    std::cout << f <<std::endl; //2
  }
  catch(const std::bad_any_cast& e)
  {
    std::cout<< e.what()<<std::endl;
  }

  try
  {
    auto g = std::any_cast<float>(a); //抛出std::bad_any_cat 异常
    std::cout << g <<std::endl; //该语句不会执行
  }
  catch(const std::bad_any_cast& e)
  {
    std::cout<< e.what()<<std::endl;  //可能输出Bad any_cast
  }
  return 0;

std::variant

std::variant是类型安全的union

#include <variant>
#include <iostream>

union my_union
{
    int i;
    float f;
    char c;
};

int main()
{
    std::cout << std::boolalpha;
    std::variant<int, float, char> variant;
    //这里的variant等价于my_union
    
    //在构造的时候,如果构造过程中抛出了异常,valueless_by_exception的返回值为true
    std::cout<< variant.valueless_by_exception()<<std::endl;   //false

    {
        variant = 12;   // variant包含了int类型
        int i = std::get<int>(variant);  //使用std::get<T>可以获取所含有的值

        try
        {
            auto f = std::get<float>(variant);  //此时的值为int,所以想要获取float的时候就会抛出异常
        }
        catch (const std::bad_variant_access & exception)
        {
            std::cout << exception.what() << std::endl;
        }

        variant = 1.0f;
        auto f = std::get<float>(variant);
        std::cout << f << std::endl;  //1.0
    }
    

    {
        //还可以使用索引来获取对应的值
        auto f = std::get<1>(variant);  
        try
        {
            auto i = std::get<0>(variant);
        }
        catch (const std::bad_variant_access & exception)
        {
            std::cout << exception.what() << std::endl;
        }

        variant = 1;
        auto i = std::get<0>(variant);
        std::cout<<i<<std::endl;  //1
    }

    variant = 2.0f;
    std::cout << variant.index() << std::endl;  //1

    variant = 2;
    std::cout << variant.index() << std::endl;  //0

    return 0;
}

std::optional

该类型是用来表示一个值是不是存在的。std::optional有两个状态,即有值和无值。通常我们将std::optional用于函数的返回值,当函数执行成功了返回有值的状态,当函数执行失败了返回无值的状态。当std::optional有值时,它可以在使用bool值的地方转化为true,反之,转化为false。

#include <iostream>
#include <optional>

int main()
{
    std::cout << std::boolalpha;
    std::optional<int> op1;       //表示一个不存在的值
    std::optional<int> op2 = 1;   //表示一个存在的int类型的值
    std::optional<int> op3(2);    //表示存在的一个int类型的值
    std::optional<std::string> op4(std::in_place, 3, 'A');     //构建一个存有std::string类型的std::optional,
                                                               //调用std::string的构造函数,其值为"AAA"
    
    std::cout << op1.has_value() << std::endl;   //输出为false
    
    if (!op1)    
    {
        std::cout << "empty optional is false" << std::endl;  //由于op1不存在值,所以在此转化为false
    }

    try 
    {
        std::cout << op4.value() << std::endl;     //获取std::optional 的值,如果不存在值,则抛出std::bad_optional_access异常
    }
    catch (std::bad_optional_access & e)
    {
        std::cout << e.what() << std::endl;
    }

    std::optional<int> op5;
    std::cout<<op5.value_or(5)<<std::endl;

    op2.reset();
    std::cout << op2.has_value() << std::endl; //false

    op2 = 3;
    op2 = 4.0;
    std::cout << op2.value() << std::endl;    //4.0
    
    op2.emplace<int>(5);
    return 0;
}

使用重载的Lambdas来vistor 访问 variant

https://blog.csdn.net/janeqi1987/article/details/100568146

template<typename... Ts> struct overload : Ts... { using Ts::operator()...; };
// base types are deduced from passed arguments:
template<typename... Ts> overloaded(Ts...) -> overloaded<Ts...>;

std::variant<double, bool, std::string> var;
var = string("hello");
// var = 1.0;
std::visit(overloaded {
            [](auto arg) { std::cout << arg << ' '; },
            [](double arg) { std::cout << std::fixed << arg << ' '; },
            [](const std::string& arg) { std::cout << std::quoted(arg) << ' '; },
        }, var);

overloads方式访问std::variant

引入的overload<> 变参模板类, 核心思路是创建一个overloaded对象, 然后从传入的多个lambda表达式继承他们的operator()操作符(Lambda表达式概念上就是提供了operator()操作符的函数对象), 这样我们就可以在std::visit()中利用lambda方便的访问对应的std::variant了.

当然, 以上代码抛开C++17的相关特性, 解释起来都费力, 所以我们下面从关联的C++17特性介绍一下实现细节.

Pack extension in using declarations

using其实早在C++11的时候就加入到标准了, 但variadic template参数展开支持using表达式, 是17才支持的特性, 像如下代码声明:

using Ts::operator()...;

借助C++17支持的using展开, 我们很容易就完成了各lambda表达式的operator()操作符的expose. 这样子类就具备了所有父类的operator()操作符, 与我们1.5中声明的那个vistor struct很接近了.

Custom template argument deduction rules(或者 user-defined template argument deduction rules)

template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>; 这个其实是一个跟c++11/14的时候加入的返回值deduce非常像的概念, 通过 user-defined template argument decduction, 我们可以告诉compiler, 括号操作符需要展开成 overloaded, 这样我们实际使用的时候就直接

overloaded {
            [](auto arg) { std::cout << arg << ' '; },
            [](double arg) { std::cout << std::fixed << arg << ' '; },
            [](const std::string& arg) { std::cout << std::quoted(arg) << ' '; },
        }

这种使用形式完成了我们的overloaded对象构造. 相关使用代码简单易读.