7

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.

Nestor
  • 687
  • 5
  • 12
  • 1
    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`) – Cruz Jean Aug 24 '19 at 23:13
  • 3
    @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. – chris Aug 24 '19 at 23:20
  • @chris The parsing functions are calling `get>` 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`) – Cruz Jean Aug 24 '19 at 23:25
  • 1
    @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. – chris Aug 24 '19 at 23:34
  • @chris 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 *anywhere* – Cruz Jean Aug 24 '19 at 23:38
  • @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>`, 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. – chris Aug 24 '19 at 23:52

2 Answers2

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 .

conr2d
  • 229
  • 2
  • 10
Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • 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 http://codereview.stackexchange.com/. It would be great if you could review it. – Nestor Aug 25 '19 at 00:14
6

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

Piotr Skotnicki
  • 46,953
  • 7
  • 118
  • 160