河北邢台企业做网站,沧州做网站最好的公司,宝塔网站搭建教程,wordpress网站备份一、可变体(variant) 基础用法
Union的问题#xff1a;
无法知道当前使用的类型是什么。而且union无法自动调用底层数据成员的析构函数。创建复杂的数据类型的封装能力非常鸡肋.
variant
C17 提供了 std::variant。
可变体的声明
下面的代码是声明一个可变体的用法…一、可变体(variant) 基础用法
Union的问题
无法知道当前使用的类型是什么。而且union无法自动调用底层数据成员的析构函数。创建复杂的数据类型的封装能力非常鸡肋.
variant
C17 提供了 std::variant。
可变体的声明
下面的代码是声明一个可变体的用法在variant关键字的尖括号内依次指定可变体的的数据类型。在可变体的内部这些数据类型存在顺序关系。
int main()
{//声明一个可变体的对象std::variantint, double, std::string tmp;
}可变体的辅助函数
C17标准中还提供了一些常用可变体的辅助函数模板的API
std::variant_size_v——用于检测可变体内部可切换的数据类型的个数。
int main()
{//声明一个可变体的对象std::variantint, double, std::string tmp;static_assert(std::variant_size_vdecltype(tmp) 3); // static_assert静态断言如果表达式为false会在编译时报错
}std::visit——用于访问可变体中的当前处于活动状态的数据类型的实例(即当前在使用的类型实例)。index方法返回当前可变体内部对应的数据类型的索引。
#includebits/stdc.hstruct 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::variantint, double, std::string tmp;static_assert(std::variant_size_vdecltype(tmp) 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简单来说就是;用来给可变体内的每一个数据类型添加上相应的动作例如
#includebits/stdc.hstruct 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::variantint, double, std::string value 123;static_assert(std::variant_size_vdecltype(value) 3, error);std::visit(PrintVisitor{}, value);return 0;
}还有一种更为高效的方式
#includebits/stdc.hint main()
{std::variantint, double, std::string value 1.123;static_assert(std::variant_size_vdecltype(value) 3, error);std::visit([](auto arg) {//using C17提供的重命名using T std::decay_tdecltype(arg); // 类型退化去掉类型中的const 以及 if constexpr(std::is_same_vT, int) { //编译时if只有被选中的if constexpr分支才会被实例化。std::cout int: arg \n;} else if constexpr(std::is_same_vT, double) { //std::is_same_v:判断输入的类型是否是指定的模板类型std::cout double: arg \n;} else if constexpr(std::is_same_vT, std::string) {std::cout string: arg \n;}}, value);return 0;
}这种方式高效的原因在于它是在编译期完成的类型判断。
std::visit的参数列表是不定长的可以传入多个variant变量
template class Visitor, class... Variants
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 —— 判断可变体当前持有的数据类型
#includebits/stdc.hstruct 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::variantint, double, std::string tmp;static_assert(std::variant_size_vdecltype(tmp) 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_ifint(tmp)); intPtr) //intPtr不为真所以不会执行std::cout int! *intPtr \n;if(const auto doublePtr (std::get_ifdouble(tmp)); doublePtr) //doublePtr不为真所以不会执行std::cout int! *doublePtr \n;if(std::holds_alternativeint(tmp))std::cout 可变体持有int类型\n;else if(std::holds_alternativedouble(tmp))std::cout 可变体持有double类型\n;else if(std::holds_alternativestd::string(tmp))std::cout 可变体持有string类型\n;
}访问可变体的异常处理
为了给可变体的访问增强类型安全在上下文可以增加bad_variant_access的异常检测。下面是一个异常处理的示例。由于当前的可变体对象内部活动类型是string。因此尝试get double(tmp)、get 0 (tmp)、get 1 (tmp)这类的访问操作都会抛出bad_variant_access异常。
#includebits/stdc.hstruct 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::variantint, double, std::string tmp;static_assert(std::variant_size_vdecltype(tmp) 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_ifint(tmp)); intPtr) //intPtr不为真所以不会执行std::cout int! *intPtr \n;if(const auto doublePtr (std::get_ifdouble(tmp)); doublePtr) //doublePtr不为真所以不会执行std::cout int! *doublePtr \n;if(std::holds_alternativeint(tmp))std::cout 可变体持有int类型\n;else if(std::holds_alternativedouble(tmp))std::cout 可变体持有double类型\n;else if(std::holds_alternativestd::string(tmp))std::cout 可变体持有string类型\n;try{/* code */auto f std::getdouble(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,int, float中的第一个ItCat获得一个默认构造器。
可变体的类型模糊的传参构造
#includebits/stdc.hclass ItCat {
public:ItCat()default;ItCat(int, float) {}
};int main()
{std::variantItCat, int, float, double tmp 1.34;std::cout tmp.index() \n;
}对于可变体声明中的参数列表int, float,double,ItCatint、float、double它们可相互转换的数据类型但对于强调类型安全的C编译器来说无疑是给它增加困扰而C编译器对待这种模棱两可的值它默认匹配值的数据类型是确保值的最大精度。因此C编译器会让可变体选择中的double类型。
std::monostate
为了支持第一个类型没有默认构造函数的variant对象提供了一个特殊的helper类型:std::monostate。类型std::monostate的对象总是具有相同的状态因此它们总是相等的。它自己的目的是表示另一种类型这样variant就没有任何其他类型的值。也就是说std::monostate可以作为第一种替代类型使变体类型默认为可构造的。
std::variantstd::monostate, NoDefConstr v2; // OK
std::cout index: v2.index() \n; // prints 0std::in_place_index函数接口
为了解决传值无棱两可的问题C17的的API库提供了std::in_place_index函数接口。下面是使用例子
#includebits/stdc.hclass ItCat {
public:ItCat()default;ItCat(int, float) {}
};int main()
{std::variantItCat, int, float, double tmp(std::in_place_index2, 1.34);std::cout tmp.index() \n;
}容器级别传值的variant构造
对于容器级传参的variant初始化问题就必须显式调用std::in_place_index告知可变体对象要在内置启用哪一个数据类型来构造可变体对象的实例。如下
#includebits/stdc.hclass ItCat {
public:ItCat()default;ItCat(int, float) {}
};int main()
{std::variantItCat, int, std::vectorint, double tmp(std::in_place_index2, {1, 2, 3, 4, 5});std::cout std::getstd::vectorint(tmp).size() \n;
}小结
默认情况下变体对象使用第一种类型进行初始化如果类型没有默认构造函数的情况下会得到一个编译器错误。在这种情况下应使用 std::monostate 将其作为第一种类型传递。
三、可变体内对象成员的生命周期和访问者模式
修改可变体的对象成员
方式1赋值操作符方式2通过get方法获取真正的对象然后修改方式3通过原地索引API匹配数据类型然后构造传值达到修改值的目的。
#includebits/stdc.hclass ItCat {
public:ItCat()default;ItCat(int, float) {}
};int main()
{using Mixtype std::variantItCat, int, std::vectorint, std::string, double;Mixtype tmp;//方式1赋值操作符tmp12; //此时为intstd::cout tmp.index() \n;std::cout std::get1(tmp) \n;tmp 23.5; //此时为doublestd::cout std::get4(tmp) \n;//方式2通过get方法获取真正的对象然后修改std::get4(tmp) 3011.7;std::cout std::get4(tmp) \n;//方式3通过原地索引API构造传值tmp Mixtype(std::in_place_index2, {42, 74, 25, 36});for(int i 0; i std::get2(tmp).size(); i) std::cout std::get2(tmp)[i] ;std::cout \n;std::get2(tmp)[0] 1024; //对容器内的单个值进行修改for(int i 0; i std::get2(tmp).size(); i) std::cout std::get2(tmp)[i] ;
}方法4emplace方法赋值。每个可变对象内置了emplace方法下面是一个具体的例子 方法4的缺点是修改可变体内部容器对象时无法对单个元素的值做精准修改。
#includebits/stdc.hclass ItCat {
public:ItCat()default;ItCat(int, float) {}
};int main()
{using Mixtype std::variantItCat, int, std::vectorint, std::string, double;Mixtype tmp;//方式1赋值操作符tmp12; //此时为intstd::cout tmp.index() \n;std::cout std::get1(tmp) \n;tmp 23.5; //此时为doublestd::cout std::get4(tmp) \n;//方式2通过get方法获取真正的对象然后修改std::get4(tmp) 3011.7;std::cout std::get4(tmp) \n;//方式3通过原地索引API构造传值tmp Mixtype(std::in_place_index2, {42, 74, 25, 36});for(int i 0; i std::get2(tmp).size(); i) std::cout std::get2(tmp)[i] ;std::cout \n;std::get2(tmp)[0] 1024; //对容器内的单个值进行修改for(int i 0; i std::get2(tmp).size(); i) std::cout std::get2(tmp)[i] ;std::cout \n;tmp.emplace2({0, 1, 2, 3, 4}); //替换下标为2的对象值for(int i 0; i std::get2(tmp).size(); i) std::cout std::get2(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 返回值
*/templateclass Vistor, class... Variants
constexpr auto visit(Vistor visitor, Variants... vars);visit的使用可以看如下例子
#includebits/stdc.hint main()
{std::variantint, double, std::string value 1.123;static_assert(std::variant_size_vdecltype(value) 3, error);std::visit([](auto arg) { //arg就是拿到的value中存的值//using C17提供的重命名using T std::decay_tdecltype(arg); // 类型退化去掉类型中的const 以及 拿到arg的类型if constexpr(std::is_same_vT, int) { //编译时if只有被选中的if constexpr分支才会被实例化。std::cout int: arg \n;} else if constexpr(std::is_same_vT, double) { //std::is_same_v:判断输入的类型是否是指定的模板类型std::cout double: arg \n;} else if constexpr(std::is_same_vT, std::string) {std::cout string: arg \n;}}, value);return 0;
}