C++17 更通用的 union:variant
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的更多相关文章
- 让Scrapy的Spider更通用
1,引言 <Scrapy的架构初探>一文所讲的Spider是整个架构中最定制化的一个部件,Spider负责把网页内容提取出来,而不同数据采集目标的内容结构不一样,几乎需要为每一类网页都做定 ...
- hdu 1005 Number Sequence(矩阵快速幂,找规律,模版更通用)
题目 第一次做是看了大牛的找规律结果,如下: //显然我看了答案,循环节点是48,但是为什么是48,据说是高手打表出来的 #include<stdio.h> int main() { ], ...
- hdu 2604 Queuing(动态规划—>矩阵快速幂,更通用的模版)
题目 最早不会写,看了网上的分析,然后终于想明白了矩阵是怎么出来的了,哈哈哈哈. 因为边上的项目排列顺序不一样,所以写出来的矩阵形式也可能不一样,但是都是可以的 //愚钝的我不会写这题,然后百度了,照 ...
- RecyclerView更通用——listView的onItemClick,onLongItemClick,addHeaderView,addFooterView
一.点击事件 setOnItemClickListener,setOnItemLongClickListener RecyclerView中虽然没有提供上面这两个接口,但是给我们提供了另外一个接口:O ...
- [UE4]更通用的接口,将UserWidget作为图标添加到小地图
将图标改成UserWidget添加到小地图,UserWidget支持动画特效,更丰富小地图的功能. 一.在小地图图标结构体中,将Flag数据类型改成UserWidget,删除ImageWidget(类 ...
- opencv中的更通用的形态学
为了处理更为复杂的情况,opencv中还支持更多的形态学变换. 形态学名称 操作过程 操作名称 是否需要temp参数 开操作 open open(src)=先腐蚀,后膨胀 CV_MOP_OPEN 否 ...
- C#不用union,而是有更好的方式实现 .net自定义错误页面实现 .net自定义错误页面实现升级篇 .net捕捉全局未处理异常的3种方式 一款很不错的FLASH时种插件 关于c#中委托使用小结 WEB网站常见受攻击方式及解决办法 判断URL是否存在 提升高并发量服务器性能解决思路
C#不用union,而是有更好的方式实现 用过C/C++的人都知道有个union,特别好用,似乎char数组到short,int,float等的转换无所不能,也确实是能,并且用起来十分方便.那C# ...
- 比最差的API(ETW)更差的API(LTTng)是如何炼成的, 谈如何写一个好的接口
最近这几天在帮柠檬看她的APM系统要如何收集.Net运行时的各种事件, 这些事件包括线程开始, JIT执行, GC触发等等. .Net在windows上(NetFramework, CoreCLR)通 ...
- 详解Mybatis通用Mapper介绍与使用
使用Mybatis的开发者,大多数都会遇到一个问题,就是要写大量的SQL在xml文件中,除了特殊的业务逻辑SQL之外,还有大量结构类似的增删改查SQL.而且,当数据库表结构改动时,对应的所有SQL以及 ...
- 《Effective Java》第8章 通用程序设计
第47条:了解和使用类库 Top 100 Java Libraries on Github 2016 Library Number of Projects Type % of projects jun ...
随机推荐
- 通过.NET Core+Vue3 实现SignalR即时通讯功能
.NET Core 和 Vue3 结合使用 SignalR 可以实现强大的实时通讯功能,允许实时双向通信.在这个示例中,我们将详细说明如何创建一个简单的聊天应用程序,演示如何使用 .NET Core ...
- [ABC282Ex] Min + Sum
Problem Statement You are given two sequences of integers of length $N$: $A = (A_1, A_2, \ldots, A_N ...
- ES集群搭建和Kibana管理集群
搭建实例 先复制2份解压后的完整目录,将里面的data和log删除. elasticsearch-6.8.23-node2 elasticsearch-6.8.23-node3 修改3个实例的配置文件 ...
- python tkinter 使用(十)
python tkinter 使用(十) #!/usr/bin/python3 # -*- coding: UTF-8 -*- """ @Author: zh @Time ...
- nginx-下载安装与配置
nginx下载 从官网下载,使用命令在linux下载即可,这个是目前稳定版最新的1.24.0版本,如果想要用旧版本直接修改版本号即可(旧版本我用的是1.12.2) 下载需要使用wget命令,默认是没有 ...
- 【scikit-learn基础】--『监督学习』之 K-近邻分类
KNN(K-近邻),全称K-Nearest Neighbors,是一种常用的分类算法.KNN算法的历史可以追溯到1957年,当时Cover和Hart提出了"最近邻分类"的概念.但是 ...
- Python——第二章:列表的概念
在编程中,列表(List)是一种常用的数据结构,用于存储一组有序的元素.列表是Python中的内置数据类型之一,它允许你在一个变量中存储多个值,并且这些值可以是不同的数据类型,包括整数.浮点数.字符串 ...
- STM32CubeMX教程10 RTC 实时时钟 - 周期唤醒、闹钟A/B事件和备份寄存器
本实验主要使用STM32CubeMX软件配置STM32F407开发板实现RTC周期唤醒.闹钟A/B事件功能,周期唤醒中输出RTC时间,闹钟A/B事件发生时利用串口输出闹钟A/B事件发生提示 1.准备材 ...
- 5分钟搞定vue3函数式弹窗
前言 最近接到一个需求,需要在一些敏感操作进行前要求输入账号和密码,然后将输入的账号和密码加到接口请求的header里面.如果每个页面都去手动导入弹窗组件,在点击按钮后弹出弹窗.再拿到弹窗返回的账号密 ...
- 文心一言 VS 讯飞星火 VS chatgpt (178)-- 算法导论13.4 1题
一.用go语言,在执行 RB-DELETE-FIXUP 之后,证明:树根一定是黑色的. 文心一言: 在证明 RB-DELETE-FIXUP 之后,树根一定是黑色的的过程中,我们首先需要理解红黑树的性质 ...