C++访问std::variant类型数据的几种方式小结

 更新时间:2024年02月20日 15:06:14   作者:AlbertS  
std::variant是 C++17中引入的一个新的类模板,提供了一种存储不同类型的值的方式,本文主要介绍了C++访问std::variant类型数据的几种方式小结,具有一定的参考价值,感兴趣的可以了解一下

前言

std::variant(可变体) 是 C++17 中引入的一个新的类模板,提供了一种存储不同类型的值的方式,类似于之前版本中的 union(联合体),但可以存储非 POD 类型和类对象,能够在运行时进行类型检查和转换,但具有更多的功能和更高的类型安全性,今天来看一下存储在std::variant中的数据要怎么读取。

variant的简单使用

可以参考cppreference网站的使用示例,也可以看看下面这个例子:

#include <iostream>
#include <variant>
#include <string>

int main()
{
    std::variant<int, double, std::string> value;

    value = 110;
    std::cout << "The value is an integer: " << std::get<int>(value) << std::endl;

    value = 0.618;
    std::cout << "The value is a double: " << std::get<double>(value) << std::endl;

    value = "hello world";
    std::cout << "The value is a string: " << std::get<std::string>(value) << std::endl;

    // value = true;        // Compilation error: cannot convert type bool to any of the alternative types
    // std::get<int>(value) // std::bad_variant_access exception: value holds a different type

    return 0;
}

在示例程序中,定义了一个 std::variant 对象 value 来存储整型、浮点型和字符串类型中的任意一种,然后分别将 value 赋值为整型、浮点型和字符串类型,并使用 std::get 来获取对应的值,此时可以正常打印 value 对象中存储的值

当我们试图将 value 赋值为其它未在定义变量时指定的类型时,编译器将会报编译错误,而当我我们试图获取 value 中不存在的类型的值时,程序将会抛出 std::bad_variant_access 异常,可以使用 try-catch 已经捕获。

通过这段代码我们可以得知,使用std::variant可以方便地存储多种类型的数据,并且能够在运行时进行类型检查和转换,这使得代码更加清晰易读,便于维护。

variant相关函数和类

  • 成员函数
    • index:返回 variant 中保存用类型的索引下标
    • valueless_by_exception:返回 variant 是否处于因异常导致的无值状态
    • emplace:原位构造 variant 中的值
  • 非成员函数
    • visit:通过调用variant保存类型值所提供的函数对象获取具体值
    • holds_alternative:检查某个 variant 是否当前持有某个给定类型
    • std::get:以给定索引或类型读取 variant 的值,错误时抛出异常
    • get_if:以给定索引或类型,获得指向被指向的 variant 的值的指针,错误时返回空指针
  • 辅助类
    • monostate:用作非可默认构造类型的 variant 的首个可选项的占位符类型(预防一些类型不提供无参构造函数)
    • bad_variant_access:非法地访问 variant 的值时抛出的异常
    • variant_npos:非法状态的 variant 的下标

访问std::variant数据

从前面提到的例子和函数说明,我们可以看到有多种方式来访问std::variant数据,接一下来一起总结一下:

std::get搭配index函数使用

#include <iostream>
#include <variant>

int main()
{
    std::variant<double, int> value = 119;

    if (1 == value.index())
        std::cout << "The value is: " << std::get<1>(value) << std::endl;

    return 0;
}

先用 index() 查询 variant保存的类型索引,然后在通过 std::get<NUMBER>() 获取其中的值

std::get搭配std::holds_alternative函数使用

#include <iostream>
#include <variant>

int main()
{
    std::variant<double, int> value = 119;

    if (std::holds_alternative<int>(value))
        std::cout << "The value is: " << std::get<int>(value) << std::endl;

    return 0;
}

先通过 std::holds_alternative() 查询 variant保存的类型,然后在通过 std::get<TYPE>() 获取其中的值

std::get_if函数

#include <iostream>
#include <variant>

int main()
{
    std::variant<double, int> value = 119;

    if (auto p = std::get_if<int>(&value))
        std::cout << "The value is: " << *p << std::endl;

    return 0;
}

直接使用 std::get_if 函数获取对应值的指针,如果类型不匹配会返回空指针

std::visit函数

使用函数visit函数访问时,有点像使用std::sort这类函数,可以搭配自定义的结构(排序)重写operator(),让其变成可以被调用的函数对象,也可以定义lambda自带可执行特性。

自定义访问结构的写法

#include <iostream>
#include <variant>

int main()
{
    std::variant<double, int> value = 119;

    struct VisitPackage
    {
        auto operator()(const double& v) { std::cout << "The value is: " << v << std::endl; }
        auto operator()(const int& v) { std::cout << "The value is: " << v << std::endl; }
    };

    std::visit(VisitPackage(), value);

    return 0;
}

定义lambda函数组重载

#include <iostream>
#include <variant>

template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

int main()
{
    std::variant<double, int> value = 119;

    std::visit(overloaded {
      [] (const double& v) { std::cout << "The value is: " << v << std::endl; },
      [] (const int& v) { std::cout << "The value is: " << v << std::endl; }
    }, value);

    return 0;
}

这种方式将多个lambda放到一起形成重载,进而达到了访问variant数据的目的。

overloaded是什么

上文例子中的最后一个中使用到了 overloaded,这令人眼花缭乱的写法着实很诡异,不过我们可以从头来分析一下,最后两个例子中等价的两部分是

struct VisitPackage
{
    auto operator()(const double& v) { std::cout << "The value is: " << v << std::endl; }
    auto operator()(const int& v) { std::cout << "The value is: " << v << std::endl; }
};

VisitPackage()

template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

overloaded {
    [] (const double& v) { std::cout << "The value is: " << v << std::endl; },
    [] (const int& v) { std::cout << "The value is: " << v << std::endl; }
}

要想理解它们为什么等价,我们首先的得弄清楚lambda表达式是什么,在剖析lambda之前先来看看 std::visit 函数需要的参数是什么,分析std::visit的参数,先看 struct VisitPackage 结构更容易一些。

std::visit的第一个参数

通俗的来说std::visit的第一个参数需要的是一个可执行的对象,如果对象能被执行就需要实现 operator() 这个操作符,看起来像函数一样,这就是为什么在 struct VisitPackage 中定义了 operator(),并且定义了两个形成了参数不同的静态重载,作用就是为了在访问 variant 对象时适配不同的类型,在访问variant 对象时会选择最匹配的 operator() 函数,进而实现了访问variant中不同类型值行为不同的目的。

那lambda表达式能实现这个目的吗?我们接着往下看

lambda 是什么

自从 C++11 引入lambda之后,对它赞美的声音不绝于耳,那lambda表达式究竟是怎样实现的呢?真的就是一个普通的函数吗?我们看一个小例子:

int main() {
  int x = 5, y = 6;

  auto func = [&](int n) {
    return x + n;
  };

  func(7);

  return 0;
}

这是一个使用lambda表达式简单的例子,代码中定义了一个int类型参数的返回值也是int的lambda函数,作用就是将外部变量x与函数参数的和返回,我们使用 cppinsights.io 网站来将此段代码展开

int main()
{
    int x = 5;
    int y = 6;

    class __lambda_4_15
    {
    public:
        inline /*constexpr */ int operator()(int n) const
        {
            return x + n;
        }

    private:
        int & x;

    public:
        __lambda_4_15(int & _x)
        : x{_x}
        {}
    };

    __lambda_4_15 func = __lambda_4_15{x};
    func.operator()(7);
    return 0;
}

可以发现我们虽然定义了一个lambda函数,但是编译器为它生成了一个类 __lambda_4_15,生成了 int& 类型的构造函数,并实现了 operator操作符,再调用lambda函数时先生成了 __lambda_4_15类的对象,再调用类的 operator()函数 func.operator()(7);,看到这里是不是似曾相识,虽然还不是很明白,但是和struct VisitPackage的定义总是有种说不清道不明的血缘关系。

弄清楚了lambda函数的本质,现在要实现的是怎么把多个lambda函数合成一个对象,并且让他们形成重载,因为lambda函数本质上存在一个类,只需要定义一个子类,继承多个lambda表达式就可以了,其实 overloaded 这个模板类就是为了实现这个目的。

overloaded剖析

template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

一时间看起来很难理解,它来自 en.cppreference.com 中介绍 std::visit 访问 std::variant 的例子,可以换行看得更清楚一点:

// helper type for the visitor #4
template<class... Ts>
struct overloaded : Ts... { using Ts::operator()...; };
// explicit deduction guide (not needed as of C++20)
template<class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;

template<class… Ts> struct overloaded : Ts… { using Ts::operator()…; };

这是一个类模板的声明,模板的名字是overloaded

分步拆解来看:

template<class… Ts> struct overloaded

  • 表示类的模板参数为可变长的参数包 Ts
  • 假设 Ts 包含 T1, T2, … , TN,那么这一句声明可以展开为:template<class T1, class T2, …, class TN> struct overloaded

struct overloaded : Ts…

  • 表示类的基类为参数包 Ts 内所有的参数类型
  • 假设 Ts 包含 T1, T2, … , TN,那么这一句声明可以展开为:struct overloaded : T1, T2, …, TN

{ using Ts::operator()…; };

  • 这是一个函数体内的变长 using 声明
  • 假设 Ts 包含 T1, T2, … , TN,那么这一句声明可以展开为:{ using T1::operator(), T1::operator(), …, TN::operator(); }
  • 经过这步声明,overloaded 类的参数包 Ts 内所有的参数作为基类的成员函数operator()均被 overloaded 类引入了自己的作用域

template<class… Ts> overloaded(Ts…) -> overloaded<Ts…>;

  • 这是一个自动推断向导说明,用于帮助编译器根据 overloaded 构造器参数的类型来推导 overloaded 的模板参数类型,C++17需要,C++20已不必写
  • 它告诉编译器,如果 overloaded 构造器所有参数的类型的集合为Ts,那么 overloaded 的模板参数类型就是 Ts 所包含的所有类型
  • 如果表达式a1, a2, …, an的类型分别为T1, T2, …, TN,那么构造器表达式overloaded x{a1, a2, …, an} 推导出,overloaded的类型就是 overloaded<T1, T2, …, TN>

经过这些解释,我们可以认为在最后一个例子中可能产生了类似这样的代码:

#include <iostream>
#include <variant>

class __lambda_12_7
{
public:
    inline /*constexpr */ void operator()(const double & v) const
    {
        std::operator<<(std::cout, "The value is: ").operator<<(v).operator<<(std::endl);
    }
};

class __lambda_13_7
{
public:
    inline /*constexpr */ void operator()(const int & v) const
    {
        std::operator<<(std::cout, "The value is: ").operator<<(v).operator<<(std::endl);
    }
};

template<>
struct overloaded<__lambda_12_7, __lambda_13_7> : public __lambda_12_7, public __lambda_13_7
{
    using __lambda_12_7::operator();
    // inline /*constexpr */ void ::operator()(const double & v) const;

    using __lambda_13_7::operator();
    // inline /*constexpr */ void ::operator()(const int & v) const;
};

int main()
{
    std::variant<double, int> value = std::variant<double, int>(119);

    std::visit(overloaded{__lambda_12_7(__lambda_12_7{}), __lambda_13_7(__lambda_13_7{})}, value);
    return 0;
}

总结

  • std::variant 可以存储多个类型的值,并且它会自动处理类型转换和内存分配
  • std::variant 可以存储非 POD 类型和类对象,能够在运行时进行类型检查和转换,具有更高的类型安全性
  • 可以使用 std::visit 全局函数来访问 std::variant 中存储的值,该函数根据存储的值的类型自动选择调用哪个函数对象
  • 可以使用 std::holds_alternative 函数来检查变量中是否存储了特定的类型
  • 定义lambda函数时,编译器会为其生成一个类

 到此这篇关于C++访问std::variant类型数据的几种方式小结的文章就介绍到这了,更多相关C++访问std::variant内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • C语言扫雷游戏的实现方法

    C语言扫雷游戏的实现方法

    这篇文章主要为大家详细介绍了C语言扫雷游戏的实现方法,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-06-06
  • C语言实现电影院选座管理系统

    C语言实现电影院选座管理系统

    这篇文章主要为大家详细介绍了C语言实现电影院选座管理系统,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-12-12
  • 关于C++继承你可能会忽视的点

    关于C++继承你可能会忽视的点

    继承是面向对象三大特性之一,有些类与类之间存在特殊的关系,下面这篇文章主要给大家介绍了关于C++继承你可能会忽视的点,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2022-02-02
  • MFC之ComboBox控件用法实例教程

    MFC之ComboBox控件用法实例教程

    这篇文章主要介绍了MFC之ComboBox控件用法,包括了ComboBox控件常见的各类用法,非常具有实用价值,需要的朋友可以参考下
    2014-09-09
  • C++ 数据结构实现两个栈实现一个队列

    C++ 数据结构实现两个栈实现一个队列

    这篇文章主要介绍了详解C++ 数据结构实现两个栈实现一个队列的相关资料,需要的朋友可以参考下
    2017-03-03
  • Visual Studio 2022 安装低版本 .Net Framework的图文教程

    Visual Studio 2022 安装低版本 .Net Framework的图文教程

    这篇文章主要介绍了Visual Studio 2022 如何安装低版本的 .Net Framework,首先打开 Visual Studio Installer 可以看到vs2022 只支持安装4.6及以上的版本,那么该如何安装4.6以下的版本,下面将详细介绍,需要的朋友可以参考下
    2022-09-09
  • C语言实现宾馆管理系统课程设计

    C语言实现宾馆管理系统课程设计

    这篇文章主要为大家详细介绍了C语言实现宾馆管理系统课程设计,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-03-03
  • C++实现关系与关系矩阵的代码详解

    C++实现关系与关系矩阵的代码详解

    这篇文章主要介绍了C++实现关系与关系矩阵,功能实现包括关系的矩阵表示,关系的性质判断及关系的合成,本文结合示例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-04-04
  • C++类模板与模板类深入详解

    C++类模板与模板类深入详解

    这篇文章主要介绍了C++类模板与模板类深入详解,需要的朋友可以参考下
    2014-07-07
  • C语言实现停车管理系统

    C语言实现停车管理系统

    这篇文章主要为大家详细介绍了C语言实现停车管理系统,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-03-03

最新评论