1
\$\begingroup\$

Sometimes, you want to an type of a particular index in an std::variant, and do something with that type without having an actual object. An example use-case would be de-serialization. What do you think of the following approach?

    /**
     * @brief Used as placeholder to allow tag dispatching
     */
    template<class T>
    using empty = std::type_identity<T>;

    /**
     * @brief Find the index of the type that satisfies pred
     */
    template<class Variant, class Predicate, size_t N = std::variant_size_v<Variant>>
    constexpr size_t find_type(Predicate&& pred)
    {
        if constexpr(N != 0)
        {
            using current_type = std::variant_alternative_t<N - 1, Variant>;
            if(pred(empty<current_type>{}))
            {
                return N - 1;
            }
            else
            {
                return find_type<Variant, N - 1, Predicate>(std::forward<Predicate>(pred));
            }
        }
        else
        {
            return std::variant_npos;
        }
    }

    template<class Callback, class ... Args>
    using callback_wrapper = void (*)(Callback&&, Args&&...);

    template<class Variant, size_t N, class Callback, class ... Args>
    constexpr void assign_callback(
        std::array<callback_wrapper<Callback, Args...>, std::variant_size_v<Variant>>& values)
    {
        if constexpr(N != 0)
        {
            using current_type = std::variant_alternative_t<N - 1, Variant>;
            values[N - 1] = [](Callback&& cb, Args&&... args){
                std::move(cb)(empty<current_type>{}, std::move(args)...);
            };
            assign_callback<Variant, N - 1>(values);
        }
    }

    template<class Variant, class Callback, class ... Args>
    constexpr auto create_vtable()
    {
        constexpr auto N = std::variant_size_v<Variant>;
        std::array<callback_wrapper<Callback, Args ...>, N> ret{};
        assign_callback<Variant, std::size(ret)>(ret);
        return ret;
    }

    /**
     * @brief Calls cb for an "empty" of type with index
     */
    template<class Variant, class Callback, class ... Args>
    decltype(auto) on_type_index(size_t index, Callback&& cb, Args&&... args)
    {
        static constexpr auto vtable = create_vtable<Variant, Callback, Args...>();
        vtable[index](std::forward<Callback>(cb), std::forward<Args>(args)...);
    }
\$\endgroup\$

1 Answer 1

0
\$\begingroup\$

Sometimes, you want to an type of a particular index in an std::variant,

Yes, you can do that with std::variant_alternative_t, which you are already using in your code.

and do something with that type without having an actual object.

You can do lots of things with just types, but I think you mean you actually want to pass that type as a function parameter to another function. This is where you use std::type_identity; it provides you with an otherwise empty object but it has an associated type alias type.

I was thinking you might avoid having to use std::type_identity by allowing Predicate to be a template template parameter, then you can write:

template<class Variant, template<class> class Predicate, size_t N = std::variant_size_v<Variant>>
constexpr size_t find_type()
{
   ...
   if(Predicate<current_type>)
   ...
};

However, template template parameters can only be template classes, not template (constepxr) variables, and recursively calling find_type() with a template type is apparently impossible.

There are also some situations where std::declval() might be of use, although I don't think it would help you.

Iterating over variants

You are using recursive functions to iterate over the possible variants. That is one way to do it. Another way is to use std::apply() in some way. Consider creating a helper function that creates a std::tuple of empty types for each of the variant's types:

template<class Variant, size_t... Is>
auto empties(std::index_sequence<Is...> = std::make_index_sequence<std::variant_size_v<Variant>>()) {
    return std::tuple<empty<std::variant_alternative_t<Is, Variant>>...>{};
}

Then you can pass it a lambda that iterates over each "empty" using a fold expression:

std::apply([](auto... types){
    (some_func(types), ...);
}, empties<Variant>());

Although it doesn't look any better than your approach. However, one advantage is that it iterates over the variant's types forward, instead of your recursive functions that go backwards, which might be surprising.

Bug in find_type()

When you call find_type() recursively, you have the order of the template arguments wrong.

Don't std::move() the callback and its arguments

You should not use std::move() when calling the callback. Instead, std::forward() the arguments and the callback, and use std::invoke() to call it:

values[N - 1] = [](Callback&& cb, Args&&... args) {
    std::invoke(std::forward<Callback>(cb), std::forward<Args>(args)...);
};

Let the callback itself std::move() if so desired.

\$\endgroup\$
1
  • \$\begingroup\$ I thought std::forward only worked on function templates, and this lambda is not a generic lambda. \$\endgroup\$
    – user877329
    Sep 3, 2022 at 5:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.