二十二、C++17中的结构化绑定、std::optional、std::variant、std::any
本部分是一个小系列,介绍C++17中新引入的、用来解决各种不同返回情况的、标准库新组件。
1、C++的结构化绑定
结构化绑定structured bindings是C++17中引入的一项特性,它允许开发者方便地从元组、结构体或数组中解包数据到单独的变量。是在元组(tuple)和对组(pairs)的基础上,新扩展的一种处理多返回值的新方法。
下面例子是我用一个函数返回一个tuple元组,我是如何取出元组中的各个元素的,也就是如何解包的:
其实在C++中处理多个返回值,我这篇博文 【C++】C++中如何处理多返回值、C++中的模板、宏_如何在c 十十 中处理返回值?-****博客 已经介绍过了好几种方法,感兴趣的同学可以自行查看。这里的前两种方式不限于C++标准的版本,但是第三种结构化绑定只适用于C++17标准。所以要使第三种解包方式顺利通过编译,就得在visual studio的property页面里面设置为C++17标准,如上右图所示。
如果说本部分处理的都是确定的、多个返回值,那么对于不确定个数的返回值如何处理呢?就是下面的小标2如何处理optional数据。
2、如何存储可能存在也可能不存在的数据:std::optional
对于返回值不确定时,我们就得用std::optional了。std::optional也是C++17标准中新引进的模板类类型,用于处理那些可能存在也可能不存在的数据。要使用std::optional,首先需要包含头文件。
比如我们现在要写一个函数,这个函数的功能是读取一个文件,那这个文件是否存在?数据是否是我们期望的格式?函数是否正常读取了这个文件?此时这个函数的返回就是一个不确定的数据,可能有返回数据,也可能没有返回数据,也可能需要返回多个数据。
如果正常读取了,那就不用返回什么;如果文件就不存在,那就得返回一个能说明文件是存在还是不存在的东西;如果文件格式不正确,那得返回一个格式不正确的说明。所以函数的返回不是很确定的这种场景,就得用std::optional了。
我们先写一个不用optional的函数:
这种写法就很被动。当读取不成功时,我们也不知道什么原因,是路径错了还是格式错了,还是路径格式都没错,只是文件里面确实就是没数据,所以啥也没读出来。下面我们看看使用optional能多大程度解决这些问题:
可见使用optional代码要稍微清晰一些了,如果文件没打开或者文件格式没对,就没读到文件,我们可以用value_or解释一下返回的空字符是怎么回事。 下面是optional操作符的简单介绍:
3、如何在一个变量中存储多种类型的数据:std::variant
这也是C++17新标准库中提供给我们的模板类:std::variant。它的作用是让我们不用担心处理的确切数据类型。
我们要做的就是指定一个叫做std::variant的类型,然后列出它可能的数据类型。比如你正在解析一个文件,你不确定这是一个字符串还是一个整数还是浮点数或者是布尔数,你就把所有可能的类型都列出来。下面是variant的操作符:
下面我们用代码来说明variant的用法:
从上图variant类型对象的大小可以看出,variant对象的大小是所有类型的大小之和。确切的说是内存对齐后的大小之和。可见,variant是结构体或者类包装而来的。就是其实每种类型它都是分配了对应的内存空间了的。
而与variant非常相似的union可并不是这样的,union分配的内存是所有可能类型数据中的最大那个类型的空间大小。所以variant和union是有本质区别的。也就是说variant是将所有可能的数据类型存储为单独的变量、作为单独的成员。而union则是所有可能的数据类型共享一个内存空间。
可见variant是给我们创建了一个结构体或类,它只是将这可能的多种数据类型存储成那个类或者结构体的成员。
也所以,从技术上讲,union更有效率,但variant更加类型安全,不会造成未定义行为。
前面读取文件的例子,我们使用optional也没有很完美的写出清晰的逻辑,现在我用variant再写这个例子:
此时是不是逻辑就清晰很多、代码也优雅很多!这就是variant的使用场景。
4、如何存储任意类型的数据:std::any
就是在C++单个变量中存储任意类型的数据:std::any,这也是C++17的全新处理方式。
std::variant是需要列出你可能用到的所有类型。而std::any是不需要声明任何类型:
可见,any类型的变量在声明和初始化时,就可以完全不用管变量的类型,只有当你要解码这个变量时才需要这个变量的真正的类型。
variant让我们列出所有可能用到的类型,然后它背后是悄悄生成一个包含了所列的类型的结构体或者类,所以variant是类型安全的。
而从any的源码看,对于小类型small type(就是最底层的那些类型),any的底层和variant是完全相同的。对于大类型(就是复杂的类型),any会动态分配内存,使用void*存储到大存储空间。动态分配内存不利于性能。就是说,如果你在小类型上,比如int, float,甚至vector类或者类似的比如math库等等,使用variant或者any都一样,因为它们都是相同的工作原理。但是如果你需要更多的存储空间,any会动态分配,但variant不会。话说回来就是variant在处理较大数据时会执行得更快,因为避免了动态分配内存。
最后再展示一下any是如何动态分配内存的:
超过32个字节,any就开始动态分配内存了。所以,any是来搞笑的吗?一般不建议经常使用。