如何编写类似于std::variant开关的代码?

26

我有一些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 包含的类型?


1
如果你需要根据实际类型执行不同的操作,那么为什么你一开始需要 std::variant<std::monostate, a, b, c> 呢?这让我觉得像一个 XY 问题 - bracco23
2
您可以使用标准的 std::visit - nop666
1
@nop666,你应该把它变成答案,我不知道std::visit。 - Serve Laurijssen
@bracco23,我需要使用完全这个变量。没有其他的。 - dasfex
std::visit很糟糕。考虑到某处有一个索引,你本以为他们可以实现switch语句。漂亮、简洁和简单。 - user997112
4个回答

28
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-else 分支,你可以使用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
  // ...
}

在C++17中,您可以使用 if (auto *v = std::get_if<a>(var); v != nullptr) 来简化代码。 - Jan Schultke
@J.Schultke:比之前/旧方法更长。就可读性而言,我认为这是主观的,两种方式对我来说都不自然。幸运的是,'std::visit' 的方式没有这些缺点 :) - Jarod42
“overloaded” 是一个标准类型还是一些你自己编写的东西? - user253751
我从std::visit的示例中了解到这个。我认为有一个建议将其添加到std中,但是即使只有一两行也可以使用它 :) - Jarod42
哎呀,如果不需要std::monostate和这个overloaded的样板代码,std::visit就会非常酷。 - itmuckel

14
最简单的方法是基于当前的 std::variant::index() 进行switch。这种方法要求你的类型(std::monostateABC)始终保持相同的顺序。
// 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返回void,因为它只处理变量的当前值。如果您想修改变量,则需要让您的lambda再次返回my_variant
您可以在std::visit内部使用重载的visitor来单独处理AB。请参见std::visit以获取更多示例。

2
您可以使用标准 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);
}

0

好的,借助一些宏魔法,你可以做到这样:

#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下的兼容性。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接