C++ 可变体(variant)
创始人
2025-06-01 20:22:01
0

一、可变体(variant) 基础用法

Union的问题:

  • 无法知道当前使用的类型是什么。
  • 而且union无法自动调用底层数据成员的析构函数。
  • 创建复杂的数据类型的封装能力非常鸡肋.

variant

C++17 提供了 std::variant

可变体的声明

下面的代码是声明一个可变体的用法,在variant关键字的尖括号内,依次指定可变体的的数据类型。在可变体的内部,这些数据类型存在顺序关系

int main()
{//声明一个可变体的对象std::variant tmp;
}

可变体的辅助函数

C++17标准中还提供了一些常用可变体的辅助函数模板的API

  • std::variant_size_v——用于检测可变体内部可切换的数据类型的个数
int main()
{//声明一个可变体的对象std::variant tmp;static_assert(std::variant_size_v == 3);   // static_assert静态断言,如果表达式为false会在编译时报错  
}
  • std::visit——用于访问可变体中的当前处于活动状态的数据类型的实例(即当前在使用的类型实例)
  • index方法返回当前可变体内部对应的数据类型的索引
#includestruct PrintVisitor  {  //visitorvoid operator()(int i) {std::cout << "int: " << i << '\n';}void operator()(double i) {std::cout << "double: " << i << '\n';}void operator()(std::string i) {std::cout << "string: " << i << '\n';}
};int main()
{std::variant tmp;static_assert(std::variant_size_v == 3);// default initialized to the first alternative, should be 0std::visit(PrintVisitor {}, tmp);std::cout << "可变体的活动类型返回的index:" << tmp.index() << std::endl;tmp = 100.00;std::cout << "可变体的活动类型返回的index:" << tmp.index() << std::endl;std::visit(PrintVisitor {}, tmp);tmp = "hello super world";std::cout << "可变体的活动类型返回的index:" << tmp.index() << std::endl;std::visit(PrintVisitor {}, tmp);}
  • 当对可变体赋值的数据类型是float,那么可变体对象tmp内部就会自动切换为float。
  • 当对可变体赋值的数据类型是string,那么可变体对象tmp内部就会自动切换为string。

在这里插入图片描述

std::visit简单来说就是;用来给可变体内的每一个数据类型添加上相应的动作,例如:

#includestruct PrintVisitor {  //visitorvoid operator()(int i) {std::cout << "int: " << i << '\n';}void operator()(double i) {std::cout << "double: " << i << '\n';}void operator()(std::string i) {std::cout << "string: " << i << '\n';}
};int main()
{std::variant value = "123";static_assert(std::variant_size_v == 3, "error");std::visit(PrintVisitor{}, value);return 0;
}

在这里插入图片描述
还有一种更为高效的方式:

#includeint main()
{std::variant value = 1.123;static_assert(std::variant_size_v == 3, "error");std::visit([](auto &&arg) {//using C++17提供的重命名using T = std::decay_t;  // 类型退化,去掉类型中的const 以及 &if constexpr(std::is_same_v) {     //编译时if,只有被选中的if constexpr分支才会被实例化。std::cout << "int: " << arg << '\n';} else if constexpr(std::is_same_v) {   //std::is_same_v:判断输入的类型是否是指定的模板类型std::cout<< "double: "<< arg <<'\n';} else if constexpr(std::is_same_v) {std::cout<< "string: "<< arg <<'\n';}}, value);return 0;
}

在这里插入图片描述
这种方式高效的原因在于它是在编译期完成的类型判断。

std::visit的参数列表是不定长的,可以传入多个variant变量:

template 
constexpr visit(Visitor&& vis, Variant&&... vars);
  • std::get_if和std::get的区别
    两个方法的参数都可以是index(下标)或者T(类型)。
    当外部代码尝试获取可变体对应的数据类型的值,那么使用 std::get_if 或std::get 访问该数据类型的值(但这可能会引发bad_variant_access 异常)。通常get_if保证std::get在访问可变体时不会抛出bad_variant_access 异常,提供了访问前的类型安全判断

  • hold_alternative<> —— 判断可变体当前持有的数据类型

#includestruct PrintVisitor  {  //visitorvoid operator()(int i) {std::cout << "int: " << i << '\n';}void operator()(double i) {std::cout << "double: " << i << '\n';}void operator()(std::string i) {std::cout << "string: " << i << '\n';}
};int main()
{std::variant tmp;static_assert(std::variant_size_v == 3);// default initialized to the first alternative, should be 0std::visit(PrintVisitor {}, tmp);std::cout << "可变体的活动类型返回的index:" << tmp.index() << std::endl;tmp = 100.0f;std::cout << "可变体的活动类型返回的index:" << tmp.index() << std::endl;std::visit(PrintVisitor {}, tmp);tmp = "hello super world";std::cout << "可变体的活动类型返回的index:" << tmp.index() << std::endl;std::visit(PrintVisitor {}, tmp);//当前tmp存的是string类型值if(const auto intPtr (std::get_if(&tmp)); intPtr)    //intPtr不为真,所以不会执行std::cout << "int! " << *intPtr << '\n';if(const auto doublePtr (std::get_if(&tmp)); doublePtr)   //doublePtr不为真,所以不会执行std::cout << "int! " << *doublePtr << '\n';if(std::holds_alternative(tmp))std::cout << "可变体持有int类型\n";else if(std::holds_alternative(tmp))std::cout << "可变体持有double类型\n";else if(std::holds_alternative(tmp))std::cout << "可变体持有string类型\n";
}

在这里插入图片描述

  • 访问可变体的异常处理

为了给可变体的访问增强类型安全,在上下文可以增加bad_variant_access的异常检测。下面是一个异常处理的示例。由于当前的可变体对象内部活动类型是string。因此尝试get< double>(tmp)、get< 0 >(tmp)、get< 1 >(tmp)这类的访问操作都会抛出bad_variant_access异常。

#includestruct PrintVisitor  {  //visitorvoid operator()(int i) {std::cout << "int: " << i << '\n';}void operator()(double i) {std::cout << "double: " << i << '\n';}void operator()(std::string i) {std::cout << "string: " << i << '\n';}
};int main()
{std::variant tmp;static_assert(std::variant_size_v == 3);// default initialized to the first alternative, should be 0std::visit(PrintVisitor {}, tmp);std::cout << "可变体的活动类型返回的index:" << tmp.index() << std::endl;tmp = 100.0f;std::cout << "可变体的活动类型返回的index:" << tmp.index() << std::endl;std::visit(PrintVisitor {}, tmp);tmp = "hello super world";std::cout << "可变体的活动类型返回的index:" << tmp.index() << std::endl;std::visit(PrintVisitor {}, tmp);//当前tmp存的是string类型值if(const auto intPtr (std::get_if(&tmp)); intPtr)    //intPtr不为真,所以不会执行std::cout << "int! " << *intPtr << '\n';if(const auto doublePtr (std::get_if(&tmp)); doublePtr)   //doublePtr不为真,所以不会执行std::cout << "int! " << *doublePtr << '\n';if(std::holds_alternative(tmp))std::cout << "可变体持有int类型\n";else if(std::holds_alternative(tmp))std::cout << "可变体持有double类型\n";else if(std::holds_alternative(tmp))std::cout << "可变体持有string类型\n";try{/* code */auto f = std::get(tmp);std::cout << "double! " << f << '\n';}catch(std::bad_variant_access&){std::cout << "可变体内部当前持有的数据类型和get<>的传入参数类型不一致" << '\n';}
}

在这里插入图片描述

小结:

  • 可通过hold_alternative当前使用的类型。
  • 可变体不允许获取非活动类型的值。
  • 可变体不会发生额外的堆内存分配。
  • 可以使用std::visit对当前保留类型调用某些操作。
  • 没有通过赋值的初始化可变体,则可变体默认使用声明中的第一种类型来初始化可变体,在这种情况下,第一个声明的类型必须具有默认构造函数

二、可变体(variant)的初始化

variant的构造

针对聚合类型的variant构造

下面代码定义了ItCat这个类,并且在声明可变体的第一个类型参数就是ItCat,不用问这段代码报错的原因其实很简单,因为ItCat没有显式提供默认的构造器。
在这里插入图片描述
那么给他ItCat这个用户自定义类型加一个默认构造器,那么在可变体在初始化过程中,就能从类型参数列表中的第一个ItCat获得一个默认构造器。
在这里插入图片描述

可变体的类型模糊的传参构造

#includeclass ItCat {
public:ItCat()=default;ItCat(int, float) {}
};int main()
{std::variant tmp = 1.34;std::cout << tmp.index() << '\n';
}

在这里插入图片描述

对于可变体声明中的参数列表,int、float、double它们可相互转换的数据类型,但对于强调类型安全的C++编译器来说,无疑是给它增加困扰,而C++编译器对待这种模棱两可的值,它默认匹配值的数据类型是确保值的最大精度。因此C++编译器会让可变体选择中的double类型。

std::monostate

为了支持第一个类型没有默认构造函数的variant对象,提供了一个特殊的helper类型:std::monostate。类型std::monostate的对象总是具有相同的状态,因此,它们总是相等的。它自己的目的是表示另一种类型,这样variant就没有任何其他类型的值。也就是说,std::monostate可以作为第一种替代类型,使变体类型默认为可构造的。

std::variant v2; // OK
std::cout << "index: " << v2.index() << '\n'; // prints 0

std::in_place_index函数接口

为了解决传值无棱两可的问题,C++17的的API库提供了std::in_place_index函数接口。下面是使用例子:

#includeclass ItCat {
public:ItCat()=default;ItCat(int, float) {}
};int main()
{std::variant tmp(std::in_place_index<2>, 1.34);std::cout << tmp.index() << '\n';
}

在这里插入图片描述

容器级别传值的variant构造

对于容器级传参的variant初始化问题,就必须显式调用std::in_place_index告知可变体对象要在内置启用哪一个数据类型来构造可变体对象的实例。如下:

#includeclass ItCat {
public:ItCat()=default;ItCat(int, float) {}
};int main()
{std::variant, double> tmp(std::in_place_index<2>, {1, 2, 3, 4, 5});std::cout << std::get>(tmp).size() << '\n';
}

小结:

默认情况下,变体对象使用第一种类型进行初始化,如果类型没有默认构造函数的情况下,会得到一个编译器错误。在这种情况下,应使用 std::monostate 将其作为第一种类型传递。

三、可变体内对象成员的生命周期和访问者模式

修改可变体的对象成员

  • 方式1:赋值操作符
  • 方式2:通过get方法获取真正的对象,然后修改
  • 方式3:通过原地索引API匹配数据类型,然后构造传值达到修改值的目的。
#includeclass ItCat {
public:ItCat()=default;ItCat(int, float) {}
};int main()
{using Mixtype = std::variant, std::string, double>;Mixtype tmp;//方式1:赋值操作符tmp=12;   //此时为intstd::cout << tmp.index() << '\n';std::cout<< std::get<1>(tmp) << '\n';tmp = 23.5; //此时为doublestd::cout<< std::get<4>(tmp) << '\n';//方式2:通过get方法获取真正的对象,然后修改std::get<4>(tmp) = 3011.7;std::cout<< std::get<4>(tmp) << '\n';//方式3:通过原地索引API构造传值tmp = Mixtype(std::in_place_index<2>, {42, 74, 25, 36});for(int i = 0; i < std::get<2>(tmp).size(); ++ i) std::cout << std::get<2>(tmp)[i] << " ";std::cout << '\n';std::get<2>(tmp)[0] = 1024;  //对容器内的单个值进行修改for(int i = 0; i < std::get<2>(tmp).size(); ++ i) std::cout << std::get<2>(tmp)[i] << " ";
}

在这里插入图片描述

  • 方法4:emplace方法赋值。每个可变对象内置了emplace方法,下面是一个具体的例子:
    方法4的缺点是修改可变体内部容器对象时无法对单个元素的值做精准修改
#includeclass ItCat {
public:ItCat()=default;ItCat(int, float) {}
};int main()
{using Mixtype = std::variant, std::string, double>;Mixtype tmp;//方式1:赋值操作符tmp=12;   //此时为intstd::cout << tmp.index() << '\n';std::cout<< std::get<1>(tmp) << '\n';tmp = 23.5; //此时为doublestd::cout<< std::get<4>(tmp) << '\n';//方式2:通过get方法获取真正的对象,然后修改std::get<4>(tmp) = 3011.7;std::cout<< std::get<4>(tmp) << '\n';//方式3:通过原地索引API构造传值tmp = Mixtype(std::in_place_index<2>, {42, 74, 25, 36});for(int i = 0; i < std::get<2>(tmp).size(); ++ i) std::cout << std::get<2>(tmp)[i] << " ";std::cout << '\n';std::get<2>(tmp)[0] = 1024;  //对容器内的单个值进行修改for(int i = 0; i < std::get<2>(tmp).size(); ++ i) std::cout << std::get<2>(tmp)[i] << " ";std::cout << '\n';tmp.emplace<2>({0, 1, 2, 3, 4});  //替换下标为2的对象值for(int i = 0; i < std::get<2>(tmp).size(); ++ i) std::cout << std::get<2>(tmp)[i] << " ";
}

在这里插入图片描述

可变体的对象成员的生命周期

union无法支持其对象成员状态的自动化管理,因此必须手动调用构造函数或析构函数这很容易令程序员写出一大堆屎山代码。std::variant自动化解决对象成员的生命周期。 这意味着如果要切换当前存储对象的数据类型,则variant在切换类型之前,会调用底层类型的析构函数。下面这个示例,很好地解析了这些。

每次对可变体赋值,一旦赋值的数据类型会当前的数据类型不一致,可变体在赋值之前,它内部自动完成对当前持有的对象所占内存的垃圾回收。

std::variant 的访问者模式

std::variant 有一个重要的辅助函数接口 std::visit,这个API可以实现一个甚至多个可变体对象以引用的方式传递给,std::visit回调的函数,而这回调函数就是所谓的“访问者”,以实现一些非常复杂的业务逻辑。

下面是访问者模式的函数模板声明:

/*** @tparam Vistor      访问者函数,即visit的回调函数的函数指针* @tparam Variants    传入参数,一个或多个可变体对象的类型* @param visitor      访问者函数,即visit的回调函数* @param vars         传入参数,一个或多个可变体对象* @return constexpr auto 返回值
*/template
constexpr auto visit(Vistor&& visitor, Variants&&... vars);

visit的使用可以看如下例子:

#includeint main()
{std::variant value = 1.123;static_assert(std::variant_size_v == 3, "error");std::visit([](auto &&arg) {    //arg就是拿到的value中存的值//using C++17提供的重命名using T = std::decay_t;  // 类型退化,去掉类型中的const 以及 &,拿到arg的类型if constexpr(std::is_same_v) {     //编译时if,只有被选中的if constexpr分支才会被实例化。std::cout << "int: " << arg << '\n';} else if constexpr(std::is_same_v) {   //std::is_same_v:判断输入的类型是否是指定的模板类型std::cout<< "double: "<< arg <<'\n';} else if constexpr(std::is_same_v) {std::cout<< "string: "<< arg <<'\n';}}, value);return 0;
}

相关内容

热门资讯

最新或2023(历届)关于一二...   最新或2023(历届)12月9日是纪念一二九爱国运动的八十周年,下面是小编给大家整理的关于一二九...
最新或2023(历届)感恩节黑...  每年11月的第四个星期四是美国传统的感恩节。在美国人的心目中,感恩节的重要性仅次于圣诞节。这是一个...
最新或2023(历届)感恩节黑...   下个星期四就是感恩节了,大家都准备好感恩节的黑板报资料了吗?一起来参考下小编给大家整理的具体内容...
最新或2023(历届)中小学纪...  一二九运动是指1935年12月9日发生在北平的一次伟大的抗日救亡运动。最新或2023(历届)的一二...
最新或2023(历届)班级纪念...  中国共产党领导的一次学生爱国运动。1935年12月9日,北平(今北京)学生数千人在中国共产党领导下...
最新或2023(历届)感恩节黑...   感恩节是美国国定假日中最地道、最美国式的节日,而且它和早期美国历史最为密切相关。感恩节就在每年1...
最新或2023(历届)感恩节黑...   每年11月的第四个星期四为感恩节(英语:Thanksgiving Day)。这是美国Day)。这...
最新或2023(历届)漂亮感恩...   感恩节(Thanksgiving Day)是美国人民独创的一个古老节日,也是美国人合家欢聚的节日...
最新或2023(历届)感恩节中...   感恩节(Thanksgiving Day)是美国人民独创的一个古老节日,也是美国人合家欢聚的节日...
最新或2023(历届)最新感恩... 11月的第四个星期四是感恩节。感恩节是美国人民独创的一个古老节日,也是美国人合家欢聚的节日,因此美国...
最新或2023(历届)全国法制...   最新或2023(历届)全国法制宣传日是最新或2023(历届)12月4日 星期五,关于法制宣传日的...
最新或2023(历届)法制宣传... 中共中央、国务院于2001年4月26日转发的《中央宣传部、司法部关于在公民中开展法制宣传教育的第四个...
最新或2023(历届)小学生感...   本周四就是感恩节了,小编精心给大家整理了关于感恩节的黑板报内容:感恩节的小故事,希望对同学们都有...
最新或2023(历届)感恩节的...   感恩节是美国国民首创的一个陈旧节日,也是美国人合家欢聚的节日,下面是关于感恩节的黑板报资料介绍,...
最新或2023(历届)全国法制...   最新或2023(历届)法制宣传日的主题是:大力弘扬法治精神,共筑伟大中国梦。下面是小编给大家整理...
国庆节主题儿童画图片素材大全,...   亲爱的朋友们,转眼又是这个熟悉的国庆节,我的祝福分分秒秒,我的关心时时刻刻,就在你的身边!愿我的...
最新或2023(历届)儿童画图...   最新或2023(历届)儿童画迎国庆-欢乐国庆节,神州大地繁花似锦,祖国长空乐曲如潮,欣望江山千里...
三年级国庆节儿童画图片素材大全...   三年级国庆节儿童画-祖国妈妈,国庆国庆,普天同庆。喜气喜气,增添福气。祖国妈妈的生日就要到了,这...
迎国庆儿童画图片素材大全,庆贺...   迎国庆儿童画-庆贺国庆节,锣鼓喧天,歌舞翩跹,国庆佳节,喜临人间;碧水含情,青山带笑,快乐好运,...
国庆节儿童画图片素材大全图片,...   小朋友们,你们喜欢国庆节吗?国庆节一到我们就放长假啦,我们就可以出去玩啦,你们说不好吗?是啊,国...