How can I code something like a switch for std::variant?
Asked Answered
C

4

28

I have some var = std::variant<std::monostate, a, b, c> when a, b, c is some types.

How, at runtime, do I check what type var contains?

In the official documentation I found information that if var contains a type and I write std::get<b>(var) I get an exception. So I thought about this solution:

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
    }
  }
}

But it's so complicated and ugly! Is there a simpler way to check what type std::variant contains?

Circumrotate answered 19/8, 2020 at 7:32 Comment(5)
Why do you need a std::variant<std::monostate, a, b, c> in first place if you then need to do different things depending on the actual type? It smells like XY Problem to me.Hellbent
You can use standard std::visitOnshore
@Onshore you should turn that into answer, I did not know about std::visitOptician
@bracco23, I need to use exactly this variant. No one another.Circumrotate
std::visit is horrible. Given there is an index somewhere, you would have thought they could implement switch. Nice, clean and simple.Anacoluthia
M
15

The most simple way is to switch based on the current std::variant::index(). This approach requires your types (std::monostate, A, B, C) to always stay in the same order.

// 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;
    }

    }
}

If your callable works with any type, you can also use 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);
}

If you don't want std::visit to accept std::monostate, then just check if the index is 0. Once again, this relies on std::monostate being the first type of the variant, so it is good practice to always make it the first.

You can also detect the type using if-constexpr inside the callable. With this approach, the arguments don't have to be in the same order anymore:

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);
}

Note that the first lambda returns void because it just processes the current value of the variant. If you want to modify the variant, your lambda needs to return my_variant again.

You could use an overloaded visitor inside std::visit to handle A or B separately. See std::visit for more examples.

Mccaffrey answered 19/8, 2020 at 7:47 Comment(0)
C
30

std::visit is the way to go:

There is even overloaded to allow inlined visitor:

// 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...>;

and so:

std::visit(overloaded{
  [](std::monostate&){/*..*/},
  [](a&){/*..*/},
  [](b&){/*..*/},
  [](c&){/*..*/}
}, var);

To use chained if-branches instead, you might used 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
  // ...
}
Chemoreceptor answered 19/8, 2020 at 8:50 Comment(6)
You could do if (auto *v = std::get_if<a>(var); v != nullptr) in C++17 to keep it short.Mccaffrey
@J.Schultke: longer than previous/old way. For readability, I would say it is subjective, both seems unnatural for me. Fortunately, std::visit way doesn't have those drawback :)Chemoreceptor
Is overloaded a standard type or is it something you wrote yourself?Chloric
@user253751: I take it from std::visit's example. I think there was a proposal to add it in std. but one or 2 lines to have it anyway :)Chemoreceptor
Oh man, std::visit would be really cool, if it didn't need std::monostate and this overloaded boilerplate.Killie
Renaming overloaded to match and this helper function template <typename... Ts, typename... Fs> constexpr decltype(auto) operator| (std::variant<Ts...> const& v, match<Fs...> const& match) { return std::visit(match, v); } gives you this syntax: std::variant<int, char> myvariant; myvariant | match{ [](int a){ f(a); }. ... }; Inure
M
15

The most simple way is to switch based on the current std::variant::index(). This approach requires your types (std::monostate, A, B, C) to always stay in the same order.

// 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;
    }

    }
}

If your callable works with any type, you can also use 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);
}

If you don't want std::visit to accept std::monostate, then just check if the index is 0. Once again, this relies on std::monostate being the first type of the variant, so it is good practice to always make it the first.

You can also detect the type using if-constexpr inside the callable. With this approach, the arguments don't have to be in the same order anymore:

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);
}

Note that the first lambda returns void because it just processes the current value of the variant. If you want to modify the variant, your lambda needs to return my_variant again.

You could use an overloaded visitor inside std::visit to handle A or B separately. See std::visit for more examples.

Mccaffrey answered 19/8, 2020 at 7:47 Comment(0)
O
3

You can use standard std::visit

Usage example:

#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);
}
Onshore answered 19/8, 2020 at 7:42 Comment(0)
C
0

Well, with some macro magic, you can do something like:

#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;
}

I tested this approach with GCC and Clang, no guarantees for MSVC.

Conspire answered 5/5, 2022 at 0:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.