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

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

作者头像
fangfang
发布2021-10-29 15:30:14
2.8K0
发布2021-10-29 15:30:14
举报
文章被收录于专栏:方方的杂货铺方方的杂货铺

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
复制
std::variant<int, double, std::string> x, y;

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

1.1 赋值操作

代码语言:javascript
复制
x = 1;
y = "1.0";
x = 2.0; // overwrite value

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

代码语言:javascript
复制
std::cout << "x - " << x.index() << std::endl;
std::cout << "y - " << y.index() << std::endl;

1.3 获取std::variant中的值

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

代码语言:javascript
复制
double d = std::get<double>(x);
std::string s = std::get<2>(y);

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

代码语言:javascript
复制
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
复制
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
复制
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
复制
// 跟迭代器的使用类似,访问没有 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
复制
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
复制
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
复制
/**
 * \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
复制
/**
 * \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
复制
/**
 * \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
复制
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
复制
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
复制
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
复制
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

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

代码语言:javascript
复制
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 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. std::variant 基础用法
    • 1.1 赋值操作
      • 1.2 获取当前使用的type 在variant声明中的索引
        • 1.3 获取std::variant中的值
          • 1.4 更安全的获取方法
          • 2. std::optional的基础用法
            • 2.1 has_value()
              • 2.2 访问optional对象中的数据
              • 3. std::visit() 方式
                • 3.1 Ponder中的Visitor使用范例
                  • 3.1.1 ConvertVisitor
                    • 3.1.2 LessThanVisitor
                      • 3.1.3 EqualVisitor
                        • 3.2. overloads方式访问std::variant
                          • 3.2.1 Pack extension in using declarations
                            • 3.2.2 Custom template argument deduction rules(或者 user-defined template argument deduction rules)
                              • 3.2.3 aggregate initialization
                                • 3.2.4 结语
                                • 4.结语
                                • 参考链接
                                相关产品与服务
                                容器服务
                                腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                                领券
                                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
                                http://www.vxiaotou.com