Quantcast
Channel: The Old New Thing
Viewing all articles
Browse latest Browse all 3085

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

$
0
0

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?

The post Getting a value from a <CODE>std::variant</CODE> that matches the type fetched from another variant appeared first on The Old New Thing.


Viewing all articles
Browse latest Browse all 3085

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>