Getting a value from a std::variant that matches the type fetched from another variant

Raymond Chen

Raymond

Suppose you have two std::variant objects of the same type and you want to perform some operation on corresponding pairs of types.

using my_variant = std::variant<int, double, std::string>;

bool are_equivalent(my_variant const& left,
                    my_variant const& right)
{
  if (left.index() != right.index()) return false;

  switch (left.index())
  {
  case 0:
    return are_equivalent(std::get<0>(left),
                          std::get<0>(right));
    break;

  case 1:
    return are_equivalent(std::get<1>(left),
                          std::get<1>(right));
    break;

  default:
    return are_equivalent(std::get<2>(left),
                          std::get<2>(right));
    break;
  }
}

Okay, what’s going on here?

We have a std::variant that can hold one of three possible types. First, we see if the two variants are even holding the same types. If not, then they are definitely not equivalent.

Otherwise, we check what is in the left object by switching on the index, and then check if the corresponding contents are equivalent.

In the case I needed to do this, the variants were part of a recursive data structure, so the recursive call to are_equivalent really did recurse deeper into the data structure.

There’s a little trick hiding in the default case: That case gets hit either when the index is 2, indicating that we have a std:string, or when the index is variant_npos, indicating that the variant is in a horrible state. If it does indeed hold a string, then the calls to std::get<2> succeed, and if it’s in a horrible state, we get a bad_variant_access exception.

This is tedious code to write. Surely there must be a better way.

What I came up with was to use the visitor pattern with a templated handler.

bool are_equivalent(my_variant const& left,
                    my_variant const& right)
{
  if (left.index() != right.index()) return false;

  return std::visit([&](auto const& l)
    {
      using T = std::decay_t<decltype(l)>;
      return are_equivalent(l, std::get<T>(right));
    }, left);
}

After verifying that the indices match, we visit the variant with a generic lambda and then reverse-engineer the appropriate getter to use for the right hand side by studying the type of the thing we were given. The std::get<T> will not throw because we already validated that the types match. (On the other hand, the entire std::visit could throw if both left and right are in horrible states.)

Note that this trick fails if the variant repeats types, because the type passed to std::get is now ambiguous.

Anyway, I had to use this pattern in a few places, so I wrote a helper function:

template<typename Template, typename... Args>
decltype(auto)
get_matching_alternative(
    const std::variant<Args...>& v,
    Template&&)
{
    using T = typename std::decay_t<Template>;
    return std::get<T>(v);
}

You pass this helper the variant you have and something that represents the thing you want, and the function returns the corresponding thing from the variant. With this helper, the are_equivalent function looks like this:

bool are_equivalent(my_variant const& left,
                    my_variant const& right)
{
  if (left.index() != right.index()) return false;

  return std::visit([&](auto const& l)
    {
      return are_equivalent(l,
                   get_matching_alternative(right, l));
    });
}

I’m still not entirely happy with this, though. Maybe you can come up with something better?

Raymond Chen
Raymond Chen

Follow Raymond   

3 comments

Comments are closed.

  • Avatar
    Pierre Baillargeon

    I feel like there could be solution involving template using integer values instead of types, but I’d need to fire up a compiler to find the proper incantation. That would allow supporting variants with repeated types. It would basically encode the first solution but converting the switch to a recursive template? I’m pretty sure there is a templated way to get the number of types in a variant to begin the chain. I think the code would be slower though as it would amount to a series of comparison instead of the potentially faster switch.

    • Avatar
      sballard@netreach.com

      Welp, I’ve got truly nerd-sniped by this – I don’t even really know C++, but I can’t resist giving it a try. Hopefully the blog software doesn’t screw with the formatting TOO much… template<int N, typename… Args>bool are_equiv_helper( const std::variant<Args…>& left, const std::variant<Args…>& right){ if (left.index() == N – 1) return are_equivalent(std::get<N – 1>(left), std::get<N – 1>(right)); else return are_equiv_helper<N – 1, Args…>(left, right);}// Specialize to end the recursiontemplate<typename… Args>bool are_equiv_helper<0, Args…>( const std::variant<Args…>& left, const std::variant<Args…>& right){ return false;}template<typename… Args>bool are_equivalent( const std::variant<Args…>& left, const std::variant<Args…>& right){ if (left.index() != right.index()) return false; return are_equiv_helper<std::variant_size<std::variant<Args…>>::value, Args…>(left, right);}

      Aaand since it did mess up the formatting, maybe this will help

  • Avatar
    Stuart Dootson

    How about using the fact that std::visit will extract values from multiple variant parameters:
    bool are_equivalent(my_variant const& l, my_variant const& r){   return (l.index() == r.index()) &&      std::visit([&](auto const& l, auto const& r) { return are_equivalent(l, r); },                    l, r);}
    I think that does what you’re wanting (obligatory Godbolt link…)