前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >如何优雅的使用 std::variant 与 std::optional

如何优雅的使用 std::variant 与 std::optional

作者头像
fangfang
发布于 2021-10-29 07:30:14
发布于 2021-10-29 07:30:14
3.8K03
代码可运行
举报
文章被收录于专栏:方方的杂货铺方方的杂货铺
运行总次数:3
代码可运行

std::variant与std::optional是c++17加入的新容器,variant主要是为了提供更安全的union, 而optional除了存取T类型本身外, 还提供了一个额外的表达optional是否被设置值的状态.

其实像std::variant 与std::optional是函数式语言中比较早就存在的两种基础类型, 比如在Haskell中, optional对应的是maybe monad, 而variant对应的是either monad, 以标准库的方式加入这些概念, 明显会强化和更好的约束我们对相关概念的表达.

另外像protobuf所用的proto中, 其实也有相关的概念, 分别是oneof和optional, 一般protobuf生成器生成相关类型在C++下的处理方法是oneof转换到union加一个which值表达当前的oneof所用的是哪个类型, 而optional对于bool, int等值类型则额外附加一个bool变量(has flag), 用来表达对应值是否已经被设置.

optional和variant都是和类型(sum type, 表达的是值的个数是所有type的总和), 区别于struct所表达的积类型.

网上有不少std::variant与std::optional的介绍, 基础的部分基本都会讲到, 这里也先简单的过一下std::variant与std::optional的常规用法.

1. std::variant 基础用法

我们以如下声明为例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
std::variant<int, double, std::string> x, y;

如上简单声明类型为std::variant<>的x, y变量, 常规操作如下:

1.1 赋值操作

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
x = 1;
y = "1.0";
x = 2.0; // overwrite value

1.2 获取当前使用的type 在variant声明中的索引

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
std::cout << "x - " << x.index() << std::endl;
std::cout << "y - " << y.index() << std::endl;

1.3 获取std::variant中的值

我们可以使用std::get() 或直接std::get()来获取variant中包含的值.

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
double d = std::get<double>(x);
std::string s = std::get<2>(y);

当然, 如果std::variant中当前存储的不是对应Type的值, 则会抛出std::bad_variant_access类型的异常:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
try
{
    int i = std::get<int>(x);
}
catch (std::bad_variant_access e)
{
    std::cerr << e.what() << std::endl;
}

1.4 更安全的获取方法

除了会引发异常的std::get<>, 也有无异常的 std::get_if() 方法, 当然, 需要自行判断返回的指针类型是否为空:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int* i = std::get_if<int>(&x);
if (i == nullptr)
{
    std::cout << "wrong type" << std::endl;
}
else
{
    std::cout << "value is " << *i << std::endl;
}

2. std::optional的基础用法

刚才也介绍过std::optional是一种sum type, 除了类型T, 它还有一个特殊的类型 std::nullopt_t, 这个类型与std::nullptr_t一样, 只有一个值, std::nullopt, optional在没有设置值的情况下类型就是std::nulopt_t, 值为std::nullopt.

2.1 has_value()

我们可以通过has_value()来判断对应的optional是否处于已经设置值的状态, 代码如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int main()
{
  std::string text = /*...*/;
  std::optional<unsigned> opt = firstEvenNumberIn(text);
  if (opt.has_value()) 
  {
    std::cout << "The first even number is "
              << opt.value()
              << ".\n";
  }
}

2.2 访问optional对象中的数据

我们可以通过value(), value_or()来获取optional对象中存储的值, value_or()可以允许传入一个默认值, 如果optional为std::nullopt, 则直接返回传入的默认值. 另外也可以像迭代器一样使用*操作符直接获取值. 需要注意的是当访问没有value的optional的时候, 行为是未定义的.

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 跟迭代器的使用类似,访问没有 value 的 optional 的行为是未定义的
cout << (*ret).out1 << endl; 
cout << ret->out1 << endl;

// 当没有 value 时调用该方法将 throws std::bad_optional_access 异常
cout << ret.value().out1 << endl;

// 当没有 value 调用该方法时将使用传入的默认值
Out defaultVal;
cout << ret.value_or(defaultVal).out1 << endl;

3. std::visit() 方式

对于optional来说, 简单的获取值的方法足够用了, 但对于更复杂的std::variant, 上面介绍的访问方式在std::variant中包含的类型较多的时候, 业务代码写起来会特别的费力, 标准库提供了通过std::visit来访问variant的方式, 这也是大多数库对variant应用所使用的方式. 对比简单的get方式来说, std::visit相对来说能够更好的适配各个使用场合(比如ponder[一个开源的C++反射库]中作为统一类型用的ponder::Value对象就提供了不同种类的vistor来完成各种功能, 后续会有相关的示例介绍). visit的使用也很简单, 通过重载的operator()操作符, 我们可以完成对std::variant<>对象所包含的各种值的处理, 我们先来看一个简单的例子再来看看更复杂的ponder中的Visitor的实现:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
std::variant<double, bool, std::string> var;
struct {
    void operator()(int) { std::cout << "int!\n"; }
    void operator()(std::string const&) { std::cout << "string!\n"; }
} visitor;

std::visit(visitor, var);

3.1 Ponder中的Visitor使用范例

前面我们介绍了std::variant, 现在结合ponder的Vistor实现来看一下具体的Vistor使用例子. ponder中的Vistor主要有三个, ConvertVisitor, LessThanVisitor,以及EqualVisitor, 分别完成ponder::Value对类型转换, <, 以及=的支持.

需要注意的是区别于前面的单参数operator()操作符, ponder中的LessThanVisitor和EqualVisitor都是双参数的, 这个其实使用也比较简单:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
std::variant<int> abc, def;
abc = 3;
def = 4;
bool testval = std::visit(overloaded{
    [](int a, int b) {
        return a < b;
    },
}, abc, def);

std::visit本身是一个variadic template的实现, 我们在std::visit调用的时候传入多个参数即可完成双操作数的visit, 同时我们也可以正确的获取std::visit调用的返回值.

3.1.1 ConvertVisitor

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * \brief Value visitor which converts the stored value to a type T
 */
template <typename T>
struct ConvertVisitor
{
    using result_type = T;

    template <typename U>
    T operator()(const U& value) const
    {
        // Dispatch to the proper ValueConverter
        return ponder_ext::ValueMapper<T>::from(value);
    }

    // Optimization when source type is the same as requested type
    T operator()(const T& value) const
    {
        return value;
    }
    T operator()(T&& value) const
    {
        return std::move(value);
    }

    T operator()(NoType) const
    {
        // Error: trying to convert an empty value
        PONDER_ERROR(BadType(ValueKind::None, mapType<T>()));
    }
};

因为重载即是各种情况的分支处理, 重载参数的类型决定调用的分支, 存储的值类型与目标值不一致的时候, 会直接使用ponder_ext中封装的ValueMapper<>来完成U到T的转换(转换失败会直接抛异常). 其它还提供了对const T&, T&&的支持.

3.1.2 LessThanVisitor

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * \brief Binary value visitor which compares two values using operator <
 */
struct LessThanVisitor
{
    using result_type = bool;

    template <typename T, typename U>
    bool operator()(const T&, const U&) const
    {
        // Different types : compare types identifiers
        return mapType<T>() < mapType<U>();
    }

    template <typename T>
    bool operator()(const T& v1, const T& v2) const
    {
        // Same types : compare values
        return v1 < v2;
    }

    bool operator()(NoType, NoType) const
    {
        // No type (empty values) : they're considered equal
        return false;
    }
};

用于完成两个ponder::Value的比较, 分了类型相同, 类型不同的情况.

3.1.3 EqualVisitor

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * \brief Binary value visitor which compares two values using operator ==
 */
struct EqualVisitor
{
    using result_type = bool;

    template <typename T, typename U>
    bool operator()(const T&, const U&) const
    {
        // Different types : not equal
        return false;
    }

    template <typename T>
    bool operator()(const T& v1, const T& v2) const
    {
        // Same types : compare values
        return v1 == v2;
    }

    bool operator()(NoType, NoType) const
    {
        // No type (empty values) : they're considered equal
        return true;
    }
};

判断两个Value是否相等. 与operator<()的实现基本类似.

3.2. overloads方式访问std::variant

除了上述介绍的方法, 有没有更优雅的使用std::visit的方式呢? 答案是显然的, cppreference上的std::visit示例代码和参考链接中的第二篇就介绍了这种方法, 并与rust的enum做了简单对比, 通过引入的两行代码, 即能优雅的实现对std::variant的访问, 先贴代码再问缘由了.

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

std::variant<double, bool, std::string> var;
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);

通过引入的overload<> 变参模板类,

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

简单的两行代码, 我们的std::visit()达到了类似派发的效果, 那么这两行代码是如何实现相关的功能的呢? 这就是参考链接1中主要介绍的内容.

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

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

3.2.1 Pack extension in using declarations

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
using Ts::operator()...;

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
overloaded {
            [](auto arg) { std::cout << arg << ' '; },
            [](double arg) { std::cout << std::fixed << arg << ' '; },
            [](const std::string& arg) { std::cout << std::quoted(arg) << ' '; },
        }

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

3.2.3 aggregate initialization

{}构造方式, 通过Class {}的方式来构造一个类, 我们不需要像平时的构造函数那样在类中指定它, 直接通过{}构造方式即可完成Class的构造函数的声明.

3.2.4 结语

通过以上介绍的特性, 我们很简单的完成了overloaded设施的封装, 有兴趣了解更多细节的同学可以点击参考链接1, 阅读原文了解更多的细节, 此处就不展开了. 相关内容的讨论的过程中 @spiritsaway也提供了不少参考, 感谢感谢.

4.结语

上面我们对std::optional, std::variant做了简单的介绍, 也介绍了怎么用std::visit方式完成对std::variant的访问, 以及相关的ponde的使用示例代码, 和介绍了一个利用c++17特性实现的overloaded特性. 个人感觉C++新特性的发展对库作者的影响还是挺大的, 大家可以用更简单, 更易懂的方式去实现一些基础功能代码, 更好的借助标准来完成相关特性的开发了.

参考链接

  1. Two lines of code and three c++17 features
  2. std::variant 与 std::visit
  3. ponder source code
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
蓝桥ROS机器人之现代C++学习笔记4.3 元组
学习程序如下: #include <tuple> #include <iostream> #include <variant> auto get_student(int id) { if (id == 0) return std::make_tuple(3.8, 'A', "John"); if (id == 1) return std::make_tuple(2.9, 'C', "Jack"); if (id == 2) retur
zhangrelay
2022/04/29
2740
蓝桥ROS机器人之现代C++学习笔记4.3 元组
链表和C++ std::list详解
链表是一种在物理上非连续、非顺序的数据结构,数据元素的逻辑顺序是通过链表中的指针链接实现,其由若干节点所组成。std::list是C++中支持常数时间从容器任何位置插入和移除元素的容器,但其不支持快速的随机访问,其通常实现为双向链表。
艰默
2023/09/05
1.6K0
链表和C++ std::list详解
C++17常用新特性(十一)---折叠表达式
从C++17开始,可以使用二元操作符对形参包中的参数进行计算,这一特性主要针对可变参数模板进行提升。支持的二元操作符多达32个。例如,下面的函数将会返回传入的所有的参数的和。
CPP开发前沿
2022/06/04
1.6K0
多态实现-虚函数、函数指针以及变体
作为一名C++面试官,问的最多的问题就是说说多态的实现机制,无非想听到的答案就是虚函数以及虚函数表,也算是烂大街的问题了,稍微有点经验的候选人都会答上个一二三。今天,借助本文,我们聊聊实现多态的几个方式。
高性能架构探索
2022/08/25
9740
多态实现-虚函数、函数指针以及变体
C++那些事之玩转optional
本节将会引入5个版本的optional实现,最终揭秘C++ STL optional实现,最后给出一个小项目作为练习的例子,让大家感受step by step学习的乐趣,所有代码、答案与相关参考资料,已更新于知识星球,欢迎大家加入学习。
公众号guangcity
2023/09/02
4000
C++那些事之玩转optional
C++ 动态新闻推送 第7期
从reddit/hackernews/lobsters/meetingcpp摘抄一些c++动态。
王很水
2021/08/31
4720
C++ 动态新闻推送 第19期
从reddit/hackernews/lobsters/meetingcpp摘抄一些c++动态。
王很水
2021/08/31
3770
【C++11】 改进我们的设计模式---访问者模式
访问者模式的应用场景不多,它可以在不改变类成员的前提下定义作用于这些元素的新的操作,是一种数据元素和数据操作分离的设计模式。
CPP开发前沿
2021/11/16
4620
【C++11】 改进我们的设计模式---访问者模式
整理C/C++的可变参数
c语言中使用可变参数最熟悉应该就是printf, 其是通过...来从代码语句中表示可变化的参数表。
Rock_Lee
2020/09/21
5.6K0
C++ std::optional完全解读
在编写可选择接受或返回对象的函数的时候,通常的做法是选择一个单独的布尔值来确保函数入参或者返回对象的可用性:
艰默
2023/10/24
1.2K0
C++ std::optional完全解读
全面盘点17个C++17的高级特性
C++17是目前比较常用的版本之一,今天花时间来梳理一下17个重要特性,所有的特性也不止这么点。
公众号guangcity
2024/03/22
3.6K0
全面盘点17个C++17的高级特性
双端队列和C++ std::deque详解
双端队列实际上是队列的一种变形,队列要求只能在队尾添加元素,在队头删除元素,而双端队列在队头和队尾都可以进行添加和删除元素的操作。双端队列是限定插入和删除操作在表的两端进行的线性表。C++中提供deque容器来实现双端队列的功能。
艰默
2023/09/05
6940
双端队列和C++ std::deque详解
C++ 中文周刊 第121期
RSS https://github.com/wanghenshui/cppweeklynews/releases.atom
王很水
2024/07/30
1130
C++ 中文周刊 第121期
C++17 New Features
如果说 C++11 和 C++20 是两个改动大、影响比较深远的"大版本",那么我感觉 C++17 算是一个小版本。(推荐 vs2019,gcc8,clang10,支持 C++17)
JIFF
2020/06/09
1.1K0
C++ 动态新闻推送 第32期
从reddit/hackernews/lobsters/meetingcpp摘抄一些c++动态
王很水
2021/10/13
4630
C++17常用新特性(九)---扩展的using声明
从C++17开始,using声明语句被扩展了,声明多个标识符时可以在一行进行声明,用逗号分隔即可。如下面的代码所示:
CPP开发前沿
2022/04/13
1K0
C++17常用新特性(九)---扩展的using声明
C++反射深入浅出 - 1. ponder 反射实现分析总篇
给静态语言添加动态特性, 似乎是C++社区一件大家乐见其成的事情, 轮子也非常多, 我们不一一列举前辈们造的各种流派的轮子了, 主要还是结合我们框架用到的C++反射实现, 结合C++的新特性, 来系统的拆解目前框架中的反射实现. 另外代码最早脱胎于Ponder, 整体处理流程基本与原版一致, 所以相关的源码可以直接参考 ponder的原始代码 . 文章计划分分7篇: - [[1. c++反射深入浅出 - ponder 反射实现分析总篇]] - [[2. c++反射深入浅出 - property实现分析]] - [[3. c++反射深入浅出 - function实现分析]] - [[4. c++反射深入浅出 - 基于反射的Lua中间层实现]] - [[5. C++反射深入浅出 - 反射信息的自动生成]] - [[6. C++反射深入浅出 - 反射的其他应用]] - [[7. C++反射深入浅出 - c++20 concept 改造]]
fangfang
2022/04/01
1.3K0
C++反射深入浅出 - 1. ponder 反射实现分析总篇
现代C++教程:高速上手(四)-容器
std::array与std::vector不同的是,array对象的大小是固定的,如果容器大小是固定的,那么可以优先考虑使用std::array容器。
程序员小涛
2020/12/03
8610
C++17 std::variant 详解:概念、用法和实现细节
在C++的发展历程中,C++17带来了许多实用的新特性,其中std::variant尤为引人注目。它本质上是一种类型安全的联合体,能够在同一时刻持有多种可能类型中的某一个值。这种特性为开发者提供了极大的便利,在面对需要处理多种不同类型数据的场景时,std::variant提供了一种灵活且高效的解决方案,使得代码编写更加简洁、安全。
码事漫谈
2025/01/27
920
C++17 std::variant 详解:概念、用法和实现细节
C++ 动态新闻推送 第4期
从reddit/hackernews/lobsters/meetingcpp摘抄一些c++动态。
王很水
2021/08/31
6240
相关推荐
蓝桥ROS机器人之现代C++学习笔记4.3 元组
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文