Search code examples
c++c++17std-variant

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


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?


Solution

  • 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.