Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

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?

like image 799
dasfex Avatar asked Aug 19 '20 07:08

dasfex


People also ask

Does STD variant require RTTI?

Since this function is specific to a given type, you don't need RTTI to perform the operations required by std::any .

What is std :: variant in C++?

The class template std::variant represents a type-safe union. An instance of std::variant at any given time either holds a value of one of its alternative types, or in the case of error - no value (this state is hard to achieve, see valueless_by_exception).

Does std :: variant allocate memory?

According to cppreference ::std::variant must not allocate dynamic memory. As with unions, if a variant holds a value of some object type T, the object representation of T is allocated directly within the object representation of the variant itself. Variant is not allowed to allocate additional (dynamic) memory.

What is STD Monostate?

std::monostate is a value-semantic type, like bool — it just has one fewer value in its domain. It can be stored in variants, or containers, or set s (it's ordered!), or unordered_set s (it's hashable!), or anywhere else you might use a value-semantic type like bool . It's totally fine to make an optional<monostate> .


2 Answers

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
  // ...
}
like image 166
Jarod42 Avatar answered Oct 14 '22 10:10

Jarod42


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.

like image 23
Jan Schultke Avatar answered Oct 14 '22 11:10

Jan Schultke