Search code examples
c++c++17template-meta-programmingvariantnlohmann-json

How to iterate over the types of std::variant?


I have some variant using V = std::variant<A, B, C> and a function with the prototype V parse(const json&). The function should try to parse all the types (e.g. A, B, then C) till the first success (and it should do it implicitly, for there will be many types in time).

How to implement something of this kind?

We may use std::variant_size somehow.

Here is something close to what I need.

My solution is to list parsers of all the types explicitly.

V parse(const json& i_j)
{
using Parser = std::function<MaybeV(const json&)>;
static const auto ps = std::vector<Parser>{
  [](const auto& j)->MaybeV{return j.get<std::optional<A>>;},
  [](const auto& j)->MaybeV{return j.get<std::optional<B>>;},
  [](const auto& j)->MaybeV{return j.get<std::optional<C>>;}
};
for (const auto& p : ps)
  if (auto opt_result = p(i_j))
    return std::move(*opt_result);
throw ParseError("Can't parse");
}

Yet it may definitely be simplified, for the lambdas different only in type and what I actually need is to iterate over the types of std::variant.


Solution

  • You want compile time integers from 0 to variant's size minus 1, and possibly early exit from iterating over them.

    There are lots of ways to get compile time integers. Two of my favorites are generating a tuple of integral constants, or calling a continuation with a parameter pack of integral constants.

    Taking the tuple of integral constants version, you can use a "tuple for each" to visit each in turn.

    template<std::size_t I>
    using index_t = std::integral_constant<std::size_t, I>;
    template<std::size_t I>
    constexpr index_t<I> index{};
    
    template<std::size_t...Is>
    constexpr std::tuple< index_t<Is>... > make_indexes(std::index_sequence<Is...>){
      return std::make_tuple(index<Is>...);
    }
    template<std::size_t N>
    constexpr auto indexing_tuple = make_indexes(std::make_index_sequence<N>{});
    

    from variant size you call that. From that you call a tuple_foreach.

    The tuple_foreach emplaces the optional return value if parsing succeeds and it wasn't already parsed.

    V parse(const json& j)
    {
      auto indexes = indexing_tuple<tuple_size_v<V>>;
      std::optional<V> retval;
      tuple_foreach(indexes, [&](auto I){ // I is compile time integer
        if(retval) return;
        auto p = j.get<tuple_alternative_t<I>>();
        if(p) retval.emplace(std::move(*p));
      });
      if(!retval) throw ParseError("Can't parse");
      return std::move(*retval);
    }
    

    tuple_foreach can be found on the internet, but for completeness:

    template<std::size_t...Is, class T, class F>
    auto tuple_foreach( std::index_sequence<Is...>, T&& tup, F&& f ) {
      ( f( std::get<Is>( std::forward<T>(tup) ) ), ... );
    }
    template<class T, class F>
    auto tuple_foreach( T&& tup, F&& f ) {
      auto indexes = std::make_index_sequence< std::tuple_size_v< std::decay_t<T> > >{};
      return tuple_foreach( indexes, std::forward<T>(tup), std::forward<F>(f) );
    }
    

    which should do it in .