当前位置: 首页 > 工具软件 > VisIt > 使用案例 >

std::variant 与 std::visit

尉迟远
2023-12-01

std::variant

简介

std::variant 是c++17 引入的一个类型,其作用类似于C语言中的Union,但是比Union 的功能强大的多。
C语言中一个联合体Union 可以储存多种类型数据,但缺点有很多。比如:
1 没有可用的方法来判断Union中真实储存的类型,获取值时也是内存拷贝的结果,可能会存在问题。这就只能靠程序员人脑保证获取的值是有意义的。
2 只能储存基础数据类型,不能储存其他结构体

使用 std::variant

声明一个variant对象很容易,我们可以利用std::variant 和 std::vector 将不同类型的数据放到一起,形成类似弱类型语言的效果 (例如javascript),这一点将非常有用。

#include <variant>

   using value = std::variant<int, double, bool, std::string>;
   std::vector<value> values = {0, true, 3.1415926,"hello"};

如何取出一个std::variant对象的值呢?
有一系列的方法判断一个 std::variant 对象的类型, 例如

  1. index() 方法, 返回 variant 对象类型index,
    当一个std::variant 对象没有被初始化的时候,会发生什么呢?会默认按照第一个类型进行初始化,那如果第一个类型没有默认的构造函数会怎么样呢?编译器会报错,为了解决这个问题,std::引入了 std::monostate 作为占位的类型。也可以使用它来替代一部分 std::optional 的功能。

  2. holds_alternative<> 判断 variant 是否是某种特定类型

  3. std::get<> 方法获取类型的值,类型不正确会抛exception,模板参数可以是类型,也可以是 index值, 但必须是常量值,因为模板是编译期推断

  4. std::get_if<> 推荐使用的方法,大部分c++项目都是禁止try- catch 的,因此无异常版本更加实用。
    该方法在类型不正确会返回空,输入和输出都是指针类型


#include <variant>

using value = std::variant<int, double, bool, std::string>;
value = "hello, world";
std::cout<<value.index()<<std::end;  //3, index 从0 开始

std::cout<<std::holds_alternative<std::string><<std::end;  //true

std::get<3>(value);
std::get<std::string>(value);
  
auto* v = std::get_if<std::string>(&value);
if (v){
   std::cout<<*v<<std::end;  //"hello, world"
}

其他:

当模板参数列表与传入值匹配度相近时,会产生错误,这时就需要使用in_place_type 指定赋值的类型
比如std::variant<float,double> v = 1.0; 这时候就需要使用


std::visit

简介

std::visit 是配合std::variant的一个访问器,可以以函数式方法访问std::variant变量。

你可能疑惑:上面不是说过使用get_if<>来访问std::variant 吗? 怎么这会儿又多出来一个std::visit ? 我们先看例子
假设我们想访问一个variant 变量,根据它的类型做不同的操, 如果不使用std::visit 会怎么样?

void Print(std::variant<int, float, string>& v) {
	int index = v.index();
	if (index == 0) {
		int value = std::get<0>(v);
		cout << "int: " << value << '\n'; 
	} else if (index == 1){
	    float value = std::get<1>(v);
		cout << "float: " << f << '\n';
	} else {
		std::string str = std::get<2>(v);
		cout << "str: " << str << '\n';
	}
}

int main() {
    std::variant<int, float, string> value = 1.0;
	Print(value);
 }

以上代码使用 holds_alternative 或者 std::get_if<>也是类似的。

接下来使用 std::visit 实现这个功能

struct PrintVisitor {
  void operator()(int i) { cout << "int: " << i << '\n'; }
  void operator()(float f) { cout << "float: " << f << '\n'; }
  void operator()(const std::string& s) { cout << "str: " << s << '\n'; }
};

int main() {
  std::variant<int, float, string> value = 1.0;
  std::visit(PrintVisitor{}, value);  // 输出 float: 1.0
 }

也可以利用C++17 新增的 overloaded 模板,可以直接生成匿名访问器,简化代码, 下面的代码是等价的

int main() {
  std::variant<int, float, string> value = 1.0;
  std::visit(overloaded{
    void operator()(int i) { cout << "int: " << i << '\n'; }
    void operator()(float f) { cout << "float: " << f << '\n'; }
    void operator()(const std::string& s) { cout << "str: " << s << '\n'; }
    },value);
 }

好像也看不出多少区别?

接下来还有第三种方法来访问std::variant

int main()
{
    std::variant<int, float, string> value = 1.0;
    std::visit(
    [&](auto &&arg) {
    	using T = std::decay_t<decltype(arg)>; // 类型退化,去掉类型中的const 以及 &
    	if constexpr(std::is_same_v<T,int>) {
    		cout << "int: " << arg << '\n';
    	} else if constexpr(std::is_same_v<T,float>){
    		cout<< "float: "<< arg <<'\n';
    	} else if constexpr(std::is_same_v<T,std::string>){
    		cout<< "str: "<< arg <<'\n';
    	}
    }, value);
}

这里我们可以看出来第三种写法比第一种的优势在哪里了:编译期推断
第三种方法由于使用了constexpr 进行 if 分支的判断,因此是在编译期运行,而第一种方法是运行期进行类型判断,效率是不同的。
第二种方法和模板一样,也是编译期推断的,因此效率也是很高的,所以我们应当尽量使用 std::visit 方法来访问variant 变量

另外std::visit 还有一个好处是,它的参数列表是不定长的,我们可以传入多个variant 变量

template <class Visitor, class... Variants>
constexpr visit(Visitor&& vis, Variant&&... vars);
 类似资料: