variant原理和应用 | 知行一

variant原理和应用

variant原理和应用

variant语义

variant是一个泛化的、类型安全的union。可以保存类型不同的对象,它的特点是多类型单值。

基本用法

以c++17中的variant为例(boost中的variant和标准库的用法几乎一样),我们定义一个这种的variant:

std::variant<int, double, char> v;

这个variant可以用来存储int, double和char三种类型的数据,所以我们可以这样赋值:

可以看到类型的值可以赋给同一个variant,如果将一个非int, double和char的类型赋值给v的话则在会出现一个编译期错误,所以variant是类型安全的,variant只允许将定义时指定的那些类型赋值给它。注意,重新赋值的时候之前的对象会自动析构掉。

接下来看如何取值:

通过std::get(v)就可以获取对应的值了,不过取值的时候需要知道当前的variant具体是什么类型,如果类型不对则会抛异常。

这种需要传具体类型的访问variant的方法有局限性,有时候我们不能确切知道当前的具体类型,这种情况下该如何取值呢?
以boost库提供的访问vairant方法为例:

这种访问方法需要定义一个函数对象对每个类型的访问都写一个重载函数,如果找不到对应的重载函数则会报一个编译期错误。

这种方法更常用,因为它不需要知道具体的类型。

还可以用C++11写一个支持lambda的visitor,避免写一个函数对象,用起来可以更简单。

实现原理

虽然很多库自己实现了variant,但其实现原理是类似的,实现variant主要分为下面几步:

1.variant内部定义一个足够大的缓冲区。

足够大的意思是这个缓冲区可以用来创建variant定义时的那些类型,具体多大呢,需要借助模版元方法遍历所有的类型找出其中size最大的那个类型,找到这个最大的size之后我们就可以定义一个栈上的char数组了,这里还需要考虑内存对齐的问题。

2.通过placement new创建某一个类型的对象。

这个对象就在内部的栈上缓冲区中创建的,所有的类型共用这块缓冲区,和union类似。具体创建对象的时候用的是placement new,一个是出于性能考虑,一个出于便于回收之前的对象考虑,因为重新赋值的时候需要将之前的对象析构掉。

3.variant赋值。

赋值时需要保存当前类型的type_index, 便于后面取值的时候判断需要取值的类型和当前类型是否一致。

4.variant的析构。

析构的时候要根据type_index遍历所有的类型找到当前的类型然后调用该类型的析构函数去析构。

通过这个思路就可以实现一个基本的variant,如果需要支持更多功能的时候还需要增加一些代码,比如支持嵌套的variant之类,支持lambda访问等等。

具体的实现可以参考我的github上C++11实现的基本功能的variant:https://github.com/qicosmos/cosmos/blob/master/Variant.hpp.

应用场景

我们一般在什么场景下需要用到variant呢?varaint一般用于类型擦除,比如我们需要访问一个某个值,这个值的类型是有限个的,这时可以用variant来表示这个“可能是多个类型的值”,避免针对具体类型去写代码,可以消除强制转换,或者把复杂问题简化,比如一些异构类型没办法抽象泛化的时候,用variant泛化是很方便的。

一个典型的应用场景就是数据库表访问的场景,数据库表字段的类型是有限多个的,我们完全可以用一个variant来表示数据库字段,接着就可以像通用类型那样去访问数据表字段了,而不必关注具体的类型,把一个复杂问题变得很简单了。

如何引入到我们的工程

boost库中已经有variant了,c++17中也引入了,如果我们的编译器还只支持C++11,则可以直接用boost中的variant,boost中的variant是header only的,直接包含头文件既可以,这里涉及到另外一个问题就是如何引入boost库。

boost库是一个广泛使用具有工业强度的c++库,很多C++新标准里的新特性都是来自于boost,使用里面已经有的库,可以避免重复造轮子,节省开发测试时间,而且质量是有保证的。

那么如何引入boost呢,引入boost有两种方式:

1.直接安装

可以查看当前源中有哪个boost库, 然后直接安装:

安装之后直接在工程中引用boost头文件既可以,以variant为例,使用variant时直接include即可:

2.编译安装

从boost.org官网下载一个版本,解压之后在boost目录执行两个命令就可以了

总结

variant作为一个类型安全的union,可以帮助我们做类型擦除,从而可以避免强制转换,便于编写更加泛化通用的代码,化繁为简。

《variant原理和应用》有2个想法

  1. 关于variant的访问,记得本站有一篇通过deduce guide简化访问的贴子,感觉那种方法极其巧妙,避免了直接定义一个函数对象,而且还是C++原生

发表评论