它们必须明确地为每种可能的类型提供函数调用操作符。然后,使用相应的重载来处理当前的备选项类型。
1. 使用对象函数方式访问
例1:
#include <iostream>
#include <variant>
#include <string>
struct MyVisitor
{
void operator()(double d) const {
std::cout << d << '\n';
}
void operator()(int i) const {
std::cout << i << '\n';
}
void operator()(const std::string& s) const {
std::cout << s << '\n';
}
};
int main()
{
std::variant<int, double, std::string> var1(42), var2(3.14), var3("visit");
std::visit(MyVisitor(), var1); // calls operator() for matching int type
std::visit(MyVisitor(), var2); // calls operator() for matching double type
std::visit(MyVisitor(), var3); // calls operator() for matching std::string type
return 0;
}
结果如下:
如果操作符()不支持所有可能的类型,或者调用不明确,则visit()调用是编译时错误。还可以使用访问者修改当前类型的值(但不能分配新类型的值)。
例2:
#include <iostream>
#include <variant>
#include <string>
struct Twice
{
void operator()(double& d) const {
d *= 2;
}
void operator()(int& i) const {
i *= 2;
}
void operator()(std::string& s) const {
s = s + s;
}
};
int main()
{
std::variant<int, double, std::string> var1(42), var2(3.14), var3("visit");
std::visit(Twice(), var1); // calls operator() for matching int type
std::visit(Twice(), var2); // calls operator() for matching double type
std::visit(Twice(), var3); // calls operator() for matching std::string type
std::cout << std::get<int>(var1) << std::endl;
std::cout << std::get<double>(var2) << std::endl;
std::cout << std::get<std::string>(var3) << std::endl;
return 0;
}
结果如下:
注意,对象操作符应该为const函数,因为它们是无状态的(它们不改变它们的行为,只改变传递的值,即不改变成员变量的值)。
2. 使用泛型Lambdas访问
使用这个特性最简单的方法是使用泛型lambda,它是一个函数对象,用于任意类型:
例3:
#include <iostream>
#include <variant>
#include <string>
auto printvariant = [](const auto& val)
{
std::cout << val << std::endl;
};
int main()
{
std::variant<int, double, std::string> var1(42), var2(3.14), var3("visit");
std::visit(printvariant, var1);
std::visit(printvariant, var2);
std::visit(printvariant, var3);
return 0;
}
结果如下:
这里,泛型lambda定义了一个闭包类型,其中函数调用操作符作为成员模板:
class CompilerSpecifyClosureTypeName
{
public:
template<typename T>
auto operator() (const T& val) const
{
std::cout << val << '\n';
}
};
也可以使用lambda来修改当前选项的值:
例4:
#include <iostream>
#include <variant>
#include <string>
auto printvariant = [](const auto& val)
{
std::cout << val << std::endl;
};
int main()
{
std::variant<int, double, std::string> var1(42), var2(3.14), var3("visit");
std::visit([](auto& val) {
val = val + val;
},
var1);
std::visit([](auto& val) {
val = val + val;
},
var2);
std::visit([](auto& val) {
val = val + val;
},
var3);
std::visit(printvariant, var1);
std::visit(printvariant, var2);
std::visit(printvariant, var3);
return 0;
}
结果如下:
甚至可以使用编译时if语言特性以不同的方式处理不同的备选值:
例5:
#include <iostream>
#include <variant>
#include <string>
auto dblvar = [](auto& val)
{
if constexpr (std::is_convertible_v<decltype(val), std::string>)
{
val = val + " test";
}
else
{
val += 2;
}
};
int main()
{
std::variant<int, double, std::string> var1(42), var2(3.14), var3("visit");
std::visit(dblvar, var1);
std::visit(dblvar, var2);
std::visit(dblvar, var3);
std::cout << std::get<int>(var1) << std::endl;
std::cout << std::get<double>(var2) << std::endl;
std::cout << std::get<std::string>(var3) << std::endl;
return 0;
}
这里,对于一个std::string类型备选项,泛型lambda的调用实例化它的泛型函数调用模板来计算:
val = val + “ test”;
而对于其他类型备选项,如int或double, lambda的调用实例化其通用函数调用模板来计算:
val += 2;
结果如下:
3. 使用重载的Lambdas来访问
通过为函数对象和lambdas使用一个重载器,还可以定义一组lambdas,其中使用最佳匹配作为访问者。假设,重载器定义为重载,如下所示:
template<typename... Ts>
struct overload : Ts...
{
using Ts::operator()...;
};
// base types are deduced from passed arguments:
template<typename... Ts>
overload(Ts...) -> overload<Ts...>;
可以使用重载访问一个变量,为每个选项提供lambdas:
std::variant<int, std::string> var(42);
...
std::visit(overload{ // calls best matching lambda for current alternative
[](int i) { std::cout << "int: " << i << '\n'; },
[](const std::string& s) {
std::cout << "string: " << s << '\n'; },
},
var);
还可以使用泛型lambda。总是用最好的搭配。例如,要修改variant对象的当前类型备选项的值,可以使用重载将字符串和其他类型的值“加倍”:
auto twice = overload{
[](std::string& s) { s += s; },
[](auto& i) { i *= 2; },
};
使用此重载,对于字符串类型备选项,将添加当前值,而对于所有其他类型,将值乘以2,这演示了variant对象的以下应用程序:
std::variant<int, std::string> var(42);
std::visit(twice, var); // value 42 becomes 84
...
var = "hi";
std::visit(twice, var); // value "hi" becomes "hihi"
例 6:
#include <iostream>
#include <variant>
#include <string>
template<typename... Ts>
struct overload : Ts...
{
using Ts::operator()...;
};
template<typename... Ts>
overload(Ts...)->overload<Ts...>;
auto twice = overload{
[](std::string& s) { s += s; },
[](auto& i) { i *= 2; },
};
int main()
{
std::variant<int, std::string> var1(42) , var3("visit");
std::visit(twice, var1);
std::visit(twice, var3);
std::visit(overload{ // calls best matching lambda for current alternative
[](int i) { std::cout << "int: " << i << '\n'; },
[](const std::string& s) {
std::cout << "string: " << s << '\n'; },
},
var1);
std::visit(overload{ // calls best matching lambda for current alternative
[](int i) { std::cout << "int: " << i << '\n'; },
[](const std::string& s) {
std::cout << "string: " << s << '\n'; },
},
var3);
return 0;
}
结果如下:
4. 多个variants
std::visitor能接受不止一个variant。visitor的声明如下:
template <class Visitor, class... Variants>
constexpr ReturnType visit(Visitor&& vis, Variants&&... vars);
它将调用std::invoke在所有varisnts的对应类型:
std::invoke(std::forward<Visitor>(vis),
std::get<is>(std::forward<Variants>(vars))...)
我们举个两个variant的例子:
std::variant<LightItem, HeavyItem> basicPackA;
std::variant<LightItem, HeavyItem> basicPackB;
std::visit(overload{
[](LightItem&, LightItem& ) { cout << "2 light items\n"; },
[](LightItem&, HeavyItem& ) { cout << "light & heavy items\n"; },
[](HeavyItem&, LightItem& ) { cout << "heavy & light items\n"; },
[](HeavyItem&, HeavyItem& ) { cout << "2 heavy items\n"; },
}, basicPackA, basicPackB);
代码输出:
2 light items
如你所见,你必须提供所有组合的重载。
我们用一个表格来说明:
如果你有两个variants, std::variant<A,B,C> abc 和 std::variant<X,Y,Z> xyz,那么你必须提供9种组合:
unc(A, X);
func(A, Y);
func(A, Z);
func(B, X);
func(B, Y);
func(B, Z);
func(C, X);
func(C, Y);
func(C, Z);
td::visit不仅可以接受多种variant,而且这些variant可能是不同类型的。
为了说明这一功能,举个例子:
struct Fluid { };
struct LightItem { };
struct HeavyItem { };
struct FragileItem { };
struct GlassBox { };
struct CardboardBox { };
struct ReinforcedBox { };
struct AmortisedBox { };
variant<Fluid, LightItem, HeavyItem, FragileItem> item {
Fluid() };
variant<GlassBox, CardboardBox, ReinforcedBox, AmortisedBox> box {
CardboardBox() };
std::visit(overload{
[](Fluid&, GlassBox& ) {
cout << "fluid in a glass box\n"; },
[](Fluid&, auto ) {
cout << "warning! fluid in a wrong container!\n"; },
[](LightItem&, CardboardBox& ) {
cout << "a light item in a cardboard box\n"; },
[](LightItem&, auto ) {
cout << "a light item can be stored in any type of box, "
"but cardboard is good enough\n"; },
[](HeavyItem&, ReinforcedBox& ) {
cout << "a heavy item in a reinforced box\n"; },
[](HeavyItem&, auto ) {
cout << "warning! a heavy item should be stored "
"in a reinforced box\n"; },
[](FragileItem&, AmortisedBox& ) {
cout << "fragile item in an amortised box\n"; },
[](FragileItem&, auto ) {
cout << "warning! a fragile item should be stored "
"in an amortised box\n"; },
}, item, box);
代码输出:
warning! fluid in a wrong container!
std::visit 接收两个变量:item 和 box,然后调用适当的重载,并显示这两种类型是否兼容。这些类型非常简单,但对它们进行扩展并添加重量、大小或其他重要成员等特性是没有问题的。
理论上,我们应该编写所有的重载组合:这意味着 4*4 = 16 个函数......但我用了一个小技巧来限制它。代码只实现了 8 个 “有效 ”和 “有趣 ”的重载。
那么如何 “skip”这些重载呢?
5.如何在std::variant中skip重载
可以使用泛型 lambda 的概念来实现 “默认 ”重载函数!
std::variant<int, float, char> v1 { 's' };
std::variant<int, float, char> v2 { 10 };
std::visit(overloaded{
[](int a, int b) { },
[](int a, float b) { },
[](int a, char b) { },
[](float a, int b) { },
[](auto a, auto b) { }, // << default!
}, v1, v2);
在上面的示例中,你可以看到只有四个重载有特定的类型,我们姑且称它们为 “有效”(或 “有意义”)的重载。其余的重载由泛型 lambda(自 C++14 起可用)处理。
泛型 lambda 解析为模板函数。在编译器创建最终重载解析集时,它的优先级低于 “具体 ”函数重载。
如果访问者是作为单独的类型实现的,则可以使用泛型 lambda 的完整扩展,并使用
template <typename A, typename B>
auto operator()(A, B) { }
C++20 update:
This would also correspond to the following C++20 code leveraging abbreviated function templates syntax:
auto operator()(auto A, auto B) { }
6. 如何传递参数
如果您想向匹配函数传递一些额外的参数,该怎么办?
理论上应该是如下:
/ pass 10 to the overload?
std::visit(/*some visitor*/, myVariant, /*your param*/10);
传递10给std::visit不能工作,我们可以包装到一个独立的variant中:
std::variant<Fluid, GlassBox> packet;
std::variant<int> intParam { 200 };
std::visit(overload{
[](Fluid&, int v) {
std::cout << "fluid + " << v << '\n';
},
[](GlassBox&, int v) {
std::cout << "glass box + " << v << '\n';
}
}, packet, intParam);
这样就可以实现了。 采用这种方法,我们需要为变体所需的额外存储空间付费,但仍不算太糟。
另外一种方式是,通过函数:
写个重载函数:
void checkParam(const Fluid& item, int p) {
std::cout << "fluid + int " << p << '\n';
}
void checkParam(const GlassBox& item, int p) {
std::cout << "glass box + int " << p << '\n';
}
让我们尝试实现对这两个参数的支持。
我们可以编写一个自定义访问者函数对象,将参数封装为数据成员:
struct VisitorAndParam {
VisitorAndParam(int p) : val_(p) { }
void operator()(Fluid& fl) { checkParam(fl, val_); }
void operator()(GlassBox& glass) { checkParam(glass, val_); }
int val_ { 0 };
};
现在按照如下调用:
int par = 100;
std::visit(VisitorAndParam{par}, packet);
如你所见,此时的visitor是作为一个"代理"去调用匹配函数。
由于调用操作符相对简单且重复,我们可以将其作为一个模板函数:
// C++20:
void operator()(auto& item) { checkParam(item, val_); }
// C++17:
template <typename T>
void operator()(T& item) { checkParam(item, val_); }
第三种方式,通过lambda:
既然我们可以使用可调用函数对象,那么 lambda 也可以做类似的事情!我们可以写一个泛型 lambda 来捕获参数。现在,我们可以用下面的代码试试 std::visit:
int param = 10;
std::visit(overload{
[¶m](const auto& item) {
checkParam(item, param);
},
}, packet);
酷吗?
们可以尝试将这段代码封装到一个单独的辅助函数中:
void applyParam(const auto& var, auto param) {
std::visit(overload{
[¶m](const auto& item) {
checkParam(item, param);
},
}, var);
}
reference:
Premium Content! - C++ Stories
How To Use std::visit With Multiple Variants and Parameters - C++ Stories