C++17 提供了 std::variant。
下面的代码是声明一个可变体的用法,在variant关键字的尖括号内,依次指定可变体的的数据类型。在可变体的内部,这些数据类型存在顺序关系。
int main()
{//声明一个可变体的对象std::variant tmp;
}
C++17标准中还提供了一些常用可变体的辅助函数模板的API
int main()
{//声明一个可变体的对象std::variant tmp;static_assert(std::variant_size_v == 3); // static_assert静态断言,如果表达式为false会在编译时报错
}
#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);}
#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;
}
这种方式高效的原因在于它是在编译期完成的类型判断。
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';}
}
下面代码定义了ItCat这个类,并且在声明可变体的第一个类型参数就是ItCat,不用问这段代码报错的原因其实很简单,因为ItCat没有显式提供默认的构造器。
那么给他ItCat这个用户自定义类型加一个默认构造器,那么在可变体在初始化过程中,就能从类型参数列表
#includeclass ItCat {
public:ItCat()=default;ItCat(int, float) {}
};int main()
{std::variant tmp = 1.34;std::cout << tmp.index() << '\n';
}
对于可变体声明中的参数列表
为了支持第一个类型没有默认构造函数的variant对象,提供了一个特殊的helper类型:std::monostate。类型std::monostate的对象总是具有相同的状态,因此,它们总是相等的。它自己的目的是表示另一种类型,这样variant就没有任何其他类型的值。也就是说,std::monostate可以作为第一种替代类型,使变体类型默认为可构造的。
std::variant v2; // OK
std::cout << "index: " << v2.index() << '\n'; // prints 0
为了解决传值无棱两可的问题,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初始化问题,就必须显式调用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 将其作为第一种类型传递。
#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] << " ";
}
#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::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;
}