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的常规用法.
我们以如下声明为例:
std::variant<int, double, std::string> x, y;
如上简单声明类型为std::variant<>的x, y变量, 常规操作如下:
x = 1;
y = "1.0";
x = 2.0; // overwrite value
std::cout << "x - " << x.index() << std::endl;
std::cout << "y - " << y.index() << std::endl;
我们可以使用std::get() 或直接std::get()来获取variant中包含的值.
double d = std::get<double>(x);
std::string s = std::get<2>(y);
当然, 如果std::variant中当前存储的不是对应Type的值, 则会抛出std::bad_variant_access类型的异常:
try
{
int i = std::get<int>(x);
}
catch (std::bad_variant_access e)
{
std::cerr << e.what() << std::endl;
}
除了会引发异常的std::get<>, 也有无异常的 std::get_if() 方法, 当然, 需要自行判断返回的指针类型是否为空:
int* i = std::get_if<int>(&x);
if (i == nullptr)
{
std::cout << "wrong type" << std::endl;
}
else
{
std::cout << "value is " << *i << std::endl;
}
刚才也介绍过std::optional是一种sum type, 除了类型T, 它还有一个特殊的类型 std::nullopt_t, 这个类型与std::nullptr_t一样, 只有一个值, std::nullopt, optional在没有设置值的情况下类型就是std::nulopt_t, 值为std::nullopt.
我们可以通过has_value()来判断对应的optional是否处于已经设置值的状态, 代码如下所示:
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";
}
}
我们可以通过value(), value_or()来获取optional对象中存储的值, value_or()可以允许传入一个默认值, 如果optional为std::nullopt, 则直接返回传入的默认值. 另外也可以像迭代器一样使用*操作符直接获取值. 需要注意的是当访问没有value的optional的时候, 行为是未定义的.
// 跟迭代器的使用类似,访问没有 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;
对于optional来说, 简单的获取值的方法足够用了, 但对于更复杂的std::variant, 上面介绍的访问方式在std::variant中包含的类型较多的时候, 业务代码写起来会特别的费力, 标准库提供了通过std::visit来访问variant的方式, 这也是大多数库对variant应用所使用的方式. 对比简单的get方式来说, std::visit相对来说能够更好的适配各个使用场合(比如ponder[一个开源的C++反射库]中作为统一类型用的ponder::Value对象就提供了不同种类的vistor来完成各种功能, 后续会有相关的示例介绍). visit的使用也很简单, 通过重载的operator()操作符, 我们可以完成对std::variant<>对象所包含的各种值的处理, 我们先来看一个简单的例子再来看看更复杂的ponder中的Visitor的实现:
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);
前面我们介绍了std::variant, 现在结合ponder的Vistor实现来看一下具体的Vistor使用例子. ponder中的Vistor主要有三个, ConvertVisitor, LessThanVisitor,以及EqualVisitor, 分别完成ponder::Value对类型转换, <, 以及=的支持.
需要注意的是区别于前面的单参数operator()操作符, ponder中的LessThanVisitor和EqualVisitor都是双参数的, 这个其实使用也比较简单:
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调用的返回值.
/**
* \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&&的支持.
/**
* \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的比较, 分了类型相同, 类型不同的情况.
/**
* \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<()的实现基本类似.
除了上述介绍的方法, 有没有更优雅的使用std::visit的方式呢? 答案是显然的, cppreference上的std::visit示例代码和参考链接中的第二篇就介绍了这种方法, 并与rust的enum做了简单对比, 通过引入的两行代码, 即能优雅的实现对std::variant的访问, 先贴代码再问缘由了.
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<> 变参模板类,
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特性介绍一下实现细节.
using其实早在C++11的时候就加入到标准了, 但variadic template参数展开支持using表达式, 是17才支持的特性, 像如下代码声明:
using Ts::operator()...;
借助C++17支持的using展开, 我们很容易就完成了各lambda表达式的operator()操作符的expose. 这样子类就具备了所有父类的operator()操作符, 与我们1.5中声明的那个vistor struct很接近了.
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对象构造. 相关使用代码简单易读.
{}构造方式, 通过Class {}的方式来构造一个类, 我们不需要像平时的构造函数那样在类中指定它, 直接通过{}构造方式即可完成Class的构造函数的声明.
通过以上介绍的特性, 我们很简单的完成了overloaded设施的封装, 有兴趣了解更多细节的同学可以点击参考链接1, 阅读原文了解更多的细节, 此处就不展开了. 相关内容的讨论的过程中 @spiritsaway也提供了不少参考, 感谢感谢.
上面我们对std::optional, std::variant做了简单的介绍, 也介绍了怎么用std::visit方式完成对std::variant的访问, 以及相关的ponde的使用示例代码, 和介绍了一个利用c++17特性实现的overloaded特性. 个人感觉C++新特性的发展对库作者的影响还是挺大的, 大家可以用更简单, 更易懂的方式去实现一些基础功能代码, 更好的借助标准来完成相关特性的开发了.
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有