我如何为 std::variant 编写类似开关的代码?
How can I code something like a switch for std::variant?
我有一些 var = std::variant<std::monostate, a, b, c>
当 a, b, c
是某些类型时。
如何在运行时检查 var
包含什么类型?
在官方文档中我发现如果 var
包含 a
类型并且我写 std::get<b>(var)
我得到一个异常。所以我想到了这个解决方案:
try {
std::variant<a>(var);
// Do something
} catch(const std::bad_variant_access&) {
try {
std::variant<b>(var);
// Do something else
} catch(const std::bad_variant_access&) {
try {
std::variant<c>(var);
// Another else
} catch (const std::bad_variant_access&) {
// std::monostate
}
}
}
但它是如此复杂和丑陋!有没有更简单的方法来检查 std::variant
包含什么类型?
您可以使用标准 std::visit
用法示例:
#include <variant>
#include <iostream>
#include <type_traits>
struct a {};
struct b {};
struct c {};
int main()
{
std::variant<a, b, c> var = a{};
std::visit([](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, a>)
std::cout << "is an a" << '\n';
else if constexpr (std::is_same_v<T, b>)
std::cout << "is a b" << '\n';
else if constexpr (std::is_same_v<T, c>)
std::cout << "is a c" << '\n';
else
std::cout << "is not in variant type list" << '\n';
}, var);
}
最简单的方法是switch
基于当前的std::variant::index()
。这种方法要求您的类型(std::monostate
、A
、B
、C
)始终保持相同的顺序。
// I omitted C to keep the example simpler, the principle is the same
using my_variant = std::variant<std::monostate, A, B>;
void foo(my_variant &v) {
switch (v.index()) {
case 0: break; // do nothing because the type is std::monostate
case 1: {
doSomethingWith(std::get<A>(v));
break;
}
case 2: {
doSomethingElseWith(std::get<B>(v));
break;
}
}
}
如果您的可调用对象适用于任何类型,您还可以使用 std::visit
:
void bar(my_variant &v) {
std::visit([](auto &&arg) -> void {
// Here, arg is std::monostate, A or B
// This lambda needs to compile with all three options.
// The lambda returns void because we don't modify the variant, so
// we could also use const& arg.
}, v);
}
如果您不希望 std::visit
接受 std::monostate
,那么只需检查 index
是否为 0。再一次,这依赖于 std::monostate
作为变体的第一种类型,因此最好始终使其成为第一种。
您还可以在可调用对象中使用 if-constexpr
检测类型。使用这种方法,参数不必再按相同的顺序排列:
void bar(my_variant &v) {
std::visit([](auto &&arg) -> my_variant {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<std::monostate, T>) {
return arg; // arg is std::monostate here
}
else if constexpr (std::is_same_v<A, T>) {
return arg + arg; // arg is A here
}
else if constexpr (std::is_same_v<B, T>) {
return arg * arg; // arg is B here
}
}, v);
}
注意第一个 lambda returns void
因为它只处理变量的当前值。如果你想修改变体,你的lambda需要再次return my_variant
。
您可以在 std::visit
中使用重载访问者来单独处理 A
或 B
。有关更多示例,请参阅 std::visit
。
std::visit
是要走的路:
甚至还有overloaded
允许内联访问者:
// helper type for the visitor #4
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
// explicit deduction guide (not needed as of C++20)
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
等等:
std::visit(overloaded{
[](std::monostate&){/*..*/},
[](a&){/*..*/},
[](b&){/*..*/},
[](c&){/*..*/}
}, var);
要改为使用链式 if-branches,您可以使用 std::get_if
if (auto* v = std::get_if<a>(var)) {
// ...
} else if (auto* v = std::get_if<b>(var)) {
// ...
} else if (auto* v = std::get_if<c>(var)) {
// ...
} else { // std::monostate
// ...
}
好吧,借助一些宏魔法,您可以执行以下操作:
#include <variant>
#include <type_traits>
#include <iostream>
#define __X_CONCAT_1(x,y) x ## y
#define __X_CONCAT(x,y) __X_CONCAT_1(x,y)
template <typename T>
struct __helper { };
// extract the type from a declaration
// we use function-type magic to get that: typename __helper<void ( (declaration) )>::type
// declaration is "int &x" for example, this class template extracts "int"
template <typename T>
struct __helper<void (T)> {
using type = std::remove_reference_t<T>;
};
#define variant_if(variant, declaration) \
if (bool __X_CONCAT(variant_if_bool_, __LINE__) = true; auto * __X_CONCAT(variant_if_ptr_, __LINE__) = std::get_if<typename __helper<void ( (declaration) )>::type>(&(variant))) \
for (declaration = * __X_CONCAT(variant_if_ptr_, __LINE__); __X_CONCAT(variant_if_bool_, __LINE__); __X_CONCAT(variant_if_bool_, __LINE__) = false)
#define variant_switch(variant) if (auto &__variant_switch_v = (variant); true)
#define variant_case(x) variant_if(__variant_switch_v, x)
int main() {
std::variant<int, long> v = 12;
std::variant<int, long> w = 32l;
std::cout << "variant_if test" << std::endl;
variant_if(v, int &x) {
std::cout << "int = " << x << std::endl;
}
else variant_if(v, long &x) {
std::cout << "long = " << x << std::endl;
}
std::cout << "variant_switch test" << std::endl;
variant_switch(v) {
variant_case(int &x) {
std::cout << "int = " << x << std::endl;
variant_switch (w) {
variant_case(int &x) {
std::cout << "int = " << x << std::endl;
}
variant_case(long &x) {
std::cout << "long = " << x << std::endl;
}
}
};
variant_case(long &x) {
std::cout << "long = " << x << std::endl;
variant_switch (w) {
variant_case(int &x) {
std::cout << "int = " << x << std::endl;
}
variant_case(long &x) {
std::cout << "long = " << x << std::endl;
}
}
};
}
return 0;
}
我用 GCC 和 Clang 测试了这种方法,MSVC 没有保证。
我有一些 var = std::variant<std::monostate, a, b, c>
当 a, b, c
是某些类型时。
如何在运行时检查 var
包含什么类型?
在官方文档中我发现如果 var
包含 a
类型并且我写 std::get<b>(var)
我得到一个异常。所以我想到了这个解决方案:
try {
std::variant<a>(var);
// Do something
} catch(const std::bad_variant_access&) {
try {
std::variant<b>(var);
// Do something else
} catch(const std::bad_variant_access&) {
try {
std::variant<c>(var);
// Another else
} catch (const std::bad_variant_access&) {
// std::monostate
}
}
}
但它是如此复杂和丑陋!有没有更简单的方法来检查 std::variant
包含什么类型?
您可以使用标准 std::visit
用法示例:
#include <variant>
#include <iostream>
#include <type_traits>
struct a {};
struct b {};
struct c {};
int main()
{
std::variant<a, b, c> var = a{};
std::visit([](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, a>)
std::cout << "is an a" << '\n';
else if constexpr (std::is_same_v<T, b>)
std::cout << "is a b" << '\n';
else if constexpr (std::is_same_v<T, c>)
std::cout << "is a c" << '\n';
else
std::cout << "is not in variant type list" << '\n';
}, var);
}
最简单的方法是switch
基于当前的std::variant::index()
。这种方法要求您的类型(std::monostate
、A
、B
、C
)始终保持相同的顺序。
// I omitted C to keep the example simpler, the principle is the same
using my_variant = std::variant<std::monostate, A, B>;
void foo(my_variant &v) {
switch (v.index()) {
case 0: break; // do nothing because the type is std::monostate
case 1: {
doSomethingWith(std::get<A>(v));
break;
}
case 2: {
doSomethingElseWith(std::get<B>(v));
break;
}
}
}
如果您的可调用对象适用于任何类型,您还可以使用 std::visit
:
void bar(my_variant &v) {
std::visit([](auto &&arg) -> void {
// Here, arg is std::monostate, A or B
// This lambda needs to compile with all three options.
// The lambda returns void because we don't modify the variant, so
// we could also use const& arg.
}, v);
}
如果您不希望 std::visit
接受 std::monostate
,那么只需检查 index
是否为 0。再一次,这依赖于 std::monostate
作为变体的第一种类型,因此最好始终使其成为第一种。
您还可以在可调用对象中使用 if-constexpr
检测类型。使用这种方法,参数不必再按相同的顺序排列:
void bar(my_variant &v) {
std::visit([](auto &&arg) -> my_variant {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<std::monostate, T>) {
return arg; // arg is std::monostate here
}
else if constexpr (std::is_same_v<A, T>) {
return arg + arg; // arg is A here
}
else if constexpr (std::is_same_v<B, T>) {
return arg * arg; // arg is B here
}
}, v);
}
注意第一个 lambda returns void
因为它只处理变量的当前值。如果你想修改变体,你的lambda需要再次return my_variant
。
您可以在 std::visit
中使用重载访问者来单独处理 A
或 B
。有关更多示例,请参阅 std::visit
。
std::visit
是要走的路:
甚至还有overloaded
允许内联访问者:
// helper type for the visitor #4
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
// explicit deduction guide (not needed as of C++20)
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
等等:
std::visit(overloaded{
[](std::monostate&){/*..*/},
[](a&){/*..*/},
[](b&){/*..*/},
[](c&){/*..*/}
}, var);
要改为使用链式 if-branches,您可以使用 std::get_if
if (auto* v = std::get_if<a>(var)) {
// ...
} else if (auto* v = std::get_if<b>(var)) {
// ...
} else if (auto* v = std::get_if<c>(var)) {
// ...
} else { // std::monostate
// ...
}
好吧,借助一些宏魔法,您可以执行以下操作:
#include <variant>
#include <type_traits>
#include <iostream>
#define __X_CONCAT_1(x,y) x ## y
#define __X_CONCAT(x,y) __X_CONCAT_1(x,y)
template <typename T>
struct __helper { };
// extract the type from a declaration
// we use function-type magic to get that: typename __helper<void ( (declaration) )>::type
// declaration is "int &x" for example, this class template extracts "int"
template <typename T>
struct __helper<void (T)> {
using type = std::remove_reference_t<T>;
};
#define variant_if(variant, declaration) \
if (bool __X_CONCAT(variant_if_bool_, __LINE__) = true; auto * __X_CONCAT(variant_if_ptr_, __LINE__) = std::get_if<typename __helper<void ( (declaration) )>::type>(&(variant))) \
for (declaration = * __X_CONCAT(variant_if_ptr_, __LINE__); __X_CONCAT(variant_if_bool_, __LINE__); __X_CONCAT(variant_if_bool_, __LINE__) = false)
#define variant_switch(variant) if (auto &__variant_switch_v = (variant); true)
#define variant_case(x) variant_if(__variant_switch_v, x)
int main() {
std::variant<int, long> v = 12;
std::variant<int, long> w = 32l;
std::cout << "variant_if test" << std::endl;
variant_if(v, int &x) {
std::cout << "int = " << x << std::endl;
}
else variant_if(v, long &x) {
std::cout << "long = " << x << std::endl;
}
std::cout << "variant_switch test" << std::endl;
variant_switch(v) {
variant_case(int &x) {
std::cout << "int = " << x << std::endl;
variant_switch (w) {
variant_case(int &x) {
std::cout << "int = " << x << std::endl;
}
variant_case(long &x) {
std::cout << "long = " << x << std::endl;
}
}
};
variant_case(long &x) {
std::cout << "long = " << x << std::endl;
variant_switch (w) {
variant_case(int &x) {
std::cout << "int = " << x << std::endl;
}
variant_case(long &x) {
std::cout << "long = " << x << std::endl;
}
}
};
}
return 0;
}
我用 GCC 和 Clang 测试了这种方法,MSVC 没有保证。