How to iterate over the types of std::variant?
Asked Answered
V

2

10

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.

Vo answered 24/8, 2019 at 23:6 Comment(6)
Unless I misunderstand what you need, std::visit should work. If the variant isn't initialized you shouldn't be parsing it. If you just mean no value that's a special case (see std::variant::valueless_by_exception)Elevenses
@CruzJean, It appears to me that the OP wants to construct the variant. There's nothing to visit because it hasn't been created yet, it's the thing returned from the function.Novikoff
@Novikoff The parsing functions are calling get<std::optional<T>> for various T types on j which is i_j, the input json object. Shouldn't that already be initialized? What we're returning is just the result of the first successful parser invocation (* just dereferences the std::optional)Elevenses
@CruzJean, The question is asking how to provide all these parsing functions without explicitly listing out each type (A, B, C) again. How the functions do the parsing isn't too relevant besides the fact that they have a clear pattern that lends itself to generating them automatically from the type of the variant. The code to check for the first valid result is fine.Novikoff
@Novikoff But if it's compatible with std::visit then a generic visitor could be used and just return the A, B, C instance directly (or throw on none, etc.). That would avoid specifying any of the types anywhereElevenses
@CruzJean, I see two problems with visiting the json object. First, it needs to support visitation, and as far as I'm aware, this library does not. Second, it seems like the alternatives of the variant might work with .get<optional<T>>, but not be handed back directly in visitation. That is, you could have a type A constructible from an int, but if you visit the json and get an int, you'd still have to go through A, B, and C checking whether you can construct them from that int. If you don't mean to visit the json object, then I'm not sure what you're suggesting to visit.Novikoff
P
9

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 .

Prosecute answered 24/8, 2019 at 23:26 Comment(1)
Many thanks! It worked (unfortunately, I googled the solution of tuple_foreach). The code you posted is a way beyond my level of competence, yet I hope, I'll understand it and post a variation of it on codereview.stackexchange.com. It would be great if you could review it.Vo
T
10

Types can be processed recursively from 0 to std::variant_size_v (exclusive), with an if-constexpr limiting template instantiations:

#include <variant>
#include <optional>
#include <cstddef>
#include <utility>

using V = std::variant<A, B, C>;

template <std::size_t I = 0>
V parse(const json& j)
{
    if constexpr (I < std::variant_size_v<V>)
    {
        auto result = j.get<std::optional<std::variant_alternative_t<I, V>>>();

        return result ? std::move(*result) : parse<I + 1>(j);
    }
    throw ParseError("Can't parse");
}

DEMO

Trawler answered 25/8, 2019 at 6:46 Comment(1)
Thank you for the solution! I tried to come up with something of this kind, yet failed(Vo
P
9

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 .

Prosecute answered 24/8, 2019 at 23:26 Comment(1)
Many thanks! It worked (unfortunately, I googled the solution of tuple_foreach). The code you posted is a way beyond my level of competence, yet I hope, I'll understand it and post a variation of it on codereview.stackexchange.com. It would be great if you could review it.Vo

© 2022 - 2024 — McMap. All rights reserved.