References

std::variant 是 C++17 中,一個新加入標準函式庫的 template 容器;他的概念基本上是和 union(參考)一樣,是一個可以用來儲存多種型別資料的容器。

比如說:

std::variant<int, double> v;

就代表 v 這個变量,可以用來儲存 int 或 double 的資料,variant 內部自己會去記錄相關的資訊。

而和 union 不同的地方,variant 也是 type-safe 的,再加上有許多函式可以搭配使用,所以在使用上應該算是相對安全;另外也由於他是標準函式庫的 template class,在使用時不需要另外去宣告一個新的型別。


基本使用

如果要使用 variant 的話,程式必須先 include <variant> 這個 header 文件;而之後呢,則就是以 template 的形式,把要允許的型別指定好,就可以用了。

下面是一個簡單的例子:

std::variant<int, double, std::string> x, y;

// assign value
x = 1;
y = "1.0"; // overwrite value
x = 2.0;

這邊是宣告了 x、y 兩個變數,透過 variant 讓他們可以儲存 int、double 或 string 的資料。

而接下來,則是讓 x 去記錄一個 int 的數字 1、並讓 y 去記錄一個字串 1.0。

之後,則是把 x 改為一個 double 的數字 2.0;而這個時候,本來 x 記錄的 int 的數字 1 就會消失了。

實際上 variant 在儲存資料的時候,內部還會有一個索引值,來記錄目前是儲存哪一種類型的資料,而透過他的 index() 這個函式,也就可以知道目前是使用第幾種型別了。

例如,在上面的程式執行完後,再繼續執行下面的程式碼:

// check index
std::cout << "x - " << x.index() << std::endl;
std::cout << "y - " << y.index() << std::endl;

這樣就會得到 x 的 index 是 1、y 的 index 是 2 的結果了。


讀取資料

當要讀取 variant 的資料的時候,需要透過 std::get<>() 這個 template 函式來在編譯階段決定讀取的型別。他有兩種方法可以指定,一個給一個數字當 index,或是直接告訴他是要用哪個型別。

下面就是簡單的範例:

// read value
double d = std::get<double>(x);
std::string s = std::get<2>(y);

像上面在讀取 y 的資料時候,是告訴系統是要把 y 當成第 2 號的資料型別來讀取,也就是 std::string 了。

而如果是指定錯誤的型別的話,std::get<>() 則是會丟出一個例外狀況,沒處理的話就會讓程式當掉。下面就是這樣的例子:

// error type
try
{
int i = std::get<int>(x);
}
catch (std::bad_variant_access e)
{
std::cerr << e.what() << std::endl;
}

由於的 x 內部是儲存 double 的資料,但是這邊卻試著把他當 int 讀,所以在執行後就會丟出 std::bad_variant_access 這個例外狀況了。

而如果不想用 try-catch 來處理例外狀況的話,則可以使用 std::get_if<>() 這個函式,來取得值的指標;而如果型別不符合的話,則是會得到一個 nullptr。下面就是一個簡單的例子:

// use 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;
}

透過 visit() 來自動處理型別

如果只是透過上面提到的get<>() 來做存取,那其實 Heresy 個人會覺得用 variant 的意義感覺不算很大。

個人覺得 variant 要好用,還要搭配 std::visit() 這個函式(參考)來使用。

visit() 基本上是一個用來處理 variant 型別的函式,讓開發者不用自己根據所有可能、一種一種去切換;在使用時,需要給他一個可以處理所有可能型別的可呼叫(callable)物件、來進行操作。

比如說,這邊要可以比較快輸出上面的 x 和 y 的話,可以定義一個 SOutput 如下:

struct SOutput
{
void operator()(const int& i)
{
std::cout << i << std::endl;
} void operator()(const double& d)
{
std::cout << d << std::endl;
} void operator()(const std::string& s)
{
std::cout << s << std::endl;
}
};

可以看到,這邊有針對所有有用到型別(int、double 和 string )都去定義對應的 function call operator。之後要使用的時候,則只要呼叫:

std::visit(SOutput(), y);

這樣編譯器就會找到對應的函式來執行了!

由於這部分會在編譯階段做檢查,所以這邊的 SOutput 要確定有針對所有可能的型別,都撰寫對應的函式,如果有缺的話,在編譯階段就不會過了!這也是一種相對安全的程式寫法。

而這邊也可以透過 template 的方式,來減少重複的程式碼;像是上面的 SOutput 就可以改寫成:

struct STOutput
{
template<typename TYPE>
void operator()(const TYPE& v)
{
std::cout << v << std::endl;
}
};

如此一來,只要寫一個函式,就可以對應所有狀況了~

而如果搭配 C++14 的 Generic Lambda,則可以更簡單地寫成:

std::visit(
[](const auto& v) {std::cout << v << std::endl; },
x);

這樣應該就算是相當方便了~


不過,如果有要透過不同的型別,做不同的處理,就不能這麼方便的 Generic Lambda 了…基本上,這邊就得回到前面,自己去定義一個 callable object,然後針對需求,各自去實作對應的函式。

下面就是一個簡單的例子:

struct STwice
{
template<typename TYPE>
void operator()(TYPE& v)
{
v *= 2;
} template<>
void operator()(std::string& s)
{
s += s;
}
};

在這個 STwice 裡,如果型別 std::string 的話,他會把字串重複兩次;而如果是其他的型別的話,則是會透過 template 處理、直接乘二。

透過這樣的寫法,就可以根據不同的型別,做不同的處理了。


如果不想另外定義一個 struct 的話,其實在 cppreference 有提供一個使用多個 lambda 來組合的例子(參考);他的概念應該是使用 parameter pack 的多重繼承的方法來做的,但是他的語法在 msvc 無法正確編譯…

而他用的語法…恩,Heresy 也看不懂(應該是 User-defined deduction guides、參考)。 orz

不過,如果真的想要組合多個 lambda 的話,可以參考 lambda_util::compose() 這個實作(gist),這份程式在 MSVC2017 是可以正確運作的。

而如果把它直接拿來用的話,前面的 STwice 就可以變成下面這樣:

std::visit(
lambda_util::compose(
[](auto& v) { v *= 2; },
[](std::string& s) { s += s; }
), y);

基本上,算是好寫一點了。


這邊針對 std::variant 的介紹大概就先到這邊了。實際上,他還有一些其他函式可以用,不過這邊就先跳過了。

完整的範例程式,可以參考放在 GitHub 上的檔案:https://github.com/KHeresy/misc/blob/master/std_variant.cpp。

不過,由於 C++17 是相對新、還沒完全定案的標準,所以編譯器要相當新的版本才能支援;以 MSVC 來說,就是需要 VisualStudio 2017 才能支援,而 gcc 的 libstdc++ 則是要到 7.0 以後才支援。

而 Boost 雖然也有提供 Variant(網頁)這個函式庫,但是實際上他的語法和 C++17 的似乎是略有不同;像 Boost 的版本的 visit() 就變成是 apply_visitor(),也沒有 get_if<>() 這個函式(似乎是直接用 get<>())…

所以以現階段來說,個人是覺得還不是很適合直接正式使用吧。


另外,在 Heresy 來看,std::variant 一個可能可以拿來實用的地方,就是透過它來讓不同的資料可以放在同一個容器內、批次處理。

下面就是一個簡單的範例:

// vector
using var_t = std::variant<int, double, std::string>;
std::vector<var_t> vData = { 1, 2.0, "hi" };
for (var_t& v : vData)
{
std::visit(STwice(), v);
std::visit(SOutput(), v);
}

要做這樣的事,以往大多是要用比較複雜的繼承、抽象化來解決的;而現在有了 variant,在某些狀況下應該是可以更簡單就可以做到同樣的事了!

而相較於使用繼承會把程式分散在個別的類別中,這邊的特色是,針對不同型別的處理的程式會都集中在一起,某方面來說算是各有優缺點了。

這部分可以參考《New Tools for a More Functional C++》這份投影片。而實際上,Heresy 也是因為看了這份投影片,才來研究 variant 的。

C++17 更通用的 union:variant的更多相关文章

  1. 让Scrapy的Spider更通用

    1,引言 <Scrapy的架构初探>一文所讲的Spider是整个架构中最定制化的一个部件,Spider负责把网页内容提取出来,而不同数据采集目标的内容结构不一样,几乎需要为每一类网页都做定 ...

  2. hdu 1005 Number Sequence(矩阵快速幂,找规律,模版更通用)

    题目 第一次做是看了大牛的找规律结果,如下: //显然我看了答案,循环节点是48,但是为什么是48,据说是高手打表出来的 #include<stdio.h> int main() { ], ...

  3. hdu 2604 Queuing(动态规划—>矩阵快速幂,更通用的模版)

    题目 最早不会写,看了网上的分析,然后终于想明白了矩阵是怎么出来的了,哈哈哈哈. 因为边上的项目排列顺序不一样,所以写出来的矩阵形式也可能不一样,但是都是可以的 //愚钝的我不会写这题,然后百度了,照 ...

  4. RecyclerView更通用——listView的onItemClick,onLongItemClick,addHeaderView,addFooterView

    一.点击事件 setOnItemClickListener,setOnItemLongClickListener RecyclerView中虽然没有提供上面这两个接口,但是给我们提供了另外一个接口:O ...

  5. [UE4]更通用的接口,将UserWidget作为图标添加到小地图

    将图标改成UserWidget添加到小地图,UserWidget支持动画特效,更丰富小地图的功能. 一.在小地图图标结构体中,将Flag数据类型改成UserWidget,删除ImageWidget(类 ...

  6. opencv中的更通用的形态学

    为了处理更为复杂的情况,opencv中还支持更多的形态学变换. 形态学名称 操作过程 操作名称 是否需要temp参数 开操作 open open(src)=先腐蚀,后膨胀  CV_MOP_OPEN 否 ...

  7. C#不用union,而是有更好的方式实现 .net自定义错误页面实现 .net自定义错误页面实现升级篇 .net捕捉全局未处理异常的3种方式 一款很不错的FLASH时种插件 关于c#中委托使用小结 WEB网站常见受攻击方式及解决办法 判断URL是否存在 提升高并发量服务器性能解决思路

    C#不用union,而是有更好的方式实现   用过C/C++的人都知道有个union,特别好用,似乎char数组到short,int,float等的转换无所不能,也确实是能,并且用起来十分方便.那C# ...

  8. 比最差的API(ETW)更差的API(LTTng)是如何炼成的, 谈如何写一个好的接口

    最近这几天在帮柠檬看她的APM系统要如何收集.Net运行时的各种事件, 这些事件包括线程开始, JIT执行, GC触发等等. .Net在windows上(NetFramework, CoreCLR)通 ...

  9. 详解Mybatis通用Mapper介绍与使用

    使用Mybatis的开发者,大多数都会遇到一个问题,就是要写大量的SQL在xml文件中,除了特殊的业务逻辑SQL之外,还有大量结构类似的增删改查SQL.而且,当数据库表结构改动时,对应的所有SQL以及 ...

  10. 《Effective Java》第8章 通用程序设计

    第47条:了解和使用类库 Top 100 Java Libraries on Github 2016 Library Number of Projects Type % of projects jun ...

随机推荐

  1. 计算机网络之防火墙和Wlan配置

    一.防火墙 防火墙(firewall)是一种安全设备,它的位置一般处于企业网络边界与外网交界的地方,用于隔离不信任的数据包 准确点讲,它就是隔离外网和内网的一道屏障,用于保护内部资源信息安全的一种策略 ...

  2. Ubuntu下安装多个JDK,并设置其中一个为默认JDK

    由于使用需要,要在机器上同时安装OpenJDK 8和11,并将8设置为默认JDK 首先安装OpenJDK sudo apt-get install openjdk-8-jdk sudo apt-get ...

  3. Go语言实现GoF设计模式:适配器模式

    本文分享自华为云社区<[Go实现]实践GoF的23种设计模式:适配器模式>,作者:元闰子. 简介 适配器模式(Adapter)是最常用的结构型模式之一,在现实生活中,适配器模式也是处处可见 ...

  4. SpringBoot-Validation优雅实现参数校验

    1.是什么? 它简化了 Java Bean Validation 的集成.Java Bean Validation 通过 JSR 380,也称为 Bean Validation 2.0,是一种标准化的 ...

  5. 记录一个异常 Gradle打包项目Lombok不生效 No serializer found for class com.qbb.User and no properties discovered to create BeanSerializer......

    完整的错误: 03-Dec-2022 16:57:22.941 涓ラ噸 [http-nio-8080-exec-5] org.apache.catalina.core.StandardWrapperV ...

  6. 华企盾DSC编辑文件不加密常见问题

    1.先查看客户端日志主进程是否是加密进程.日志中是不是勾选智能半透明.加密类型是否有添加 2.用procmon监控保存的文件找出writefile的进程是否有添加,进程树是否有父进程,加密类型是否正确 ...

  7. javaScript正则截取自定义标签-javascript-zheng-ze-jie-qu-zi-ding-yi-biao-qian

    title: javaScript正则截取自定义标签 date: 2021-12-29 17:31:48.448 updated: 2021-12-29 17:31:48.448 url: https ...

  8. LLM增强LLM;通过预测上下文来提高文生图质量;Spikformer V2;同时执行刚性和非刚性编辑的通用图像编辑框架

    文章首发于公众号:机器感知 LLM增强LLM:通过预测上下文来提高文生图质量:Spikformer V2:同时执行刚性和非刚性编辑的通用图像编辑框架 LLM Augmented LLMs: Expan ...

  9. netty自定义channel id

    netty自定义channel id.netty custom channel id 搞搞netty时发现默认的id很长,无法直接自定义. 于是我网上搜索了search一下,发现没有相关文章,那就自己 ...

  10. Linux 逻辑卷管理

    如果用标准分区在硬盘上创建了文件系统,为已有的文件系统添加额外的空间是一件十分痛苦的事情.只能在已有的硬盘上的可用空间范围内调整分区大小,如果硬盘空间不够的话,就只能换一个大容量的硬盘,然后手动将已有 ...