3

Given a class that is defined to operate on a collection of complex types defined in a parameter pack, I've come up with this pattern to index the types in the parameter pack at runtime:

#include <iostream>
#include <variant>

template <typename T>
std::pair<typename T::typeK, typename T::typeV> pushdata_deserialize_impl(const uint8_t *serialized);

template <typename K, typename V>
class DataClass{
public:
    using typeK = K;
    using typeV = V;
};


template <class... DataClassCollection>
class DeSerializer{
public:
    using VariantType = std::variant<std::monostate, std::pair<typename DataClassCollection::typeK, typename DataClassCollection::typeV>...>;
    explicit DeSerializer(){}
    VariantType pushdata(const uint8_t *dat_serialized, uint8_t datatype_id){
        // deserialize based on the datatype_id, receive a variant of pairs
        VariantType dat = pushdata_deserialize_helper(static_cast<int>(datatype_id), dat_serialized);
        return dat;
    }

    // this returns a variant with monostate in it
    auto pushdata_deserialize_helper(int index, const uint8_t *serialized) {
        // returns a tuple
        VariantType result;
        size_t count = 0;
        ((index == count++ ? result = pushdata_deserialize_impl<DataClassCollection>(serialized) : std::monostate()), ...);
        return result;
    }
};

template <>
std::pair<int, int> pushdata_deserialize_impl<DataClass<int, int>>(const uint8_t *serialized) {
    std::cout<<"Called deserialize for <int, int>"<<std::endl;
    return  std::make_pair<int, int>(1, 1);
}

template <>
std::pair<int, float> pushdata_deserialize_impl<DataClass<int, float>>(const uint8_t *serialized) {
    std::cout<<"Called deserialize for <int, float>"<<std::endl;
    return  std::make_pair<int, float>(2, 2.0);
}


int main() {
    // some input array to be processed
    uint8_t serialized[3] = {0,1,2};

    // works because no repetition of feeder types
    DeSerializer<DataClass<int, int>, DataClass<int, float>> service_pushdata_working;
    service_pushdata_working.pushdata(serialized, 2);

    // does not work because there is a repetition of DataClass<int, float>
    DeSerializer<DataClass<int, int>, DataClass<int, float>, DataClass<int, float>> service_pushdata_bugged;
    service_pushdata_bugged.pushdata(serialized, 2);
    return 0;
}

The important is the line

((index == count++ ? result = pushdata_deserialize_impl<DataClassCollection>(serialized) : std::monostate()), ...);

which iterates the types in the pack and if the index matches the count, the correct implementation of pushdata_deserialize_impl is called. This works fine as long as the types in the parameter pack are unique.

However, this pattern breaks down as soon as I have duplicate types in my parameter pack, like so:

DeSerializer<DataClass<int, int>, DataClass<int, float>, DataClass<int, float>>

How can I solve this? I cannot modify the DataClass type as it is given by another codebase.

Note that while the repeating type in the parameter pack may seem useless in this context, the actual implementation will trigger different logic for each type in the parameter pack, even if the types may be repeating.

user3641187
  • 405
  • 5
  • 10
  • Would it suffice to filter out duplicates in the `variant` type? – TartanLlama Apr 18 '23 at 08:30
  • As long as the correct DeSerializer implementation is called it should suffice - ultimately, the parsed and returned "std::pair" just needs to be compatible with the downstream function signature. 2 separate (but equally defined) types in the DataClassCollection parameter pack will return the same type of std::pair using the same deserialization logic. Since we have the unique datatype_id, we can still differentiate to which downstream function the std::pair is passed. – user3641187 Apr 18 '23 at 08:56
  • To add to my above comment - while filtering out would be no issue, this should not have an influence on the provided datatype_id though. This id has to remain unique and well defined for each type in the DataClassCollection parameter pack, even if some filtering logic might resolve the issue inside VariantType. I'd also add that VariantType does support repeating data types, i.e. std::variant v(std::in_place_index<1>, d);. So I'm not sure what filtering would solve here. – user3641187 Apr 18 '23 at 09:06

2 Answers2

2

One way is to generate compile-time indices to index into your variant:

First put std::monostate at the end of the variant (or keep it at the front and do an Idx+1 later):

using VariantType = std::variant<std::pair<typename DataClassCollection::typeK, typename DataClassCollection::typeV>..., std::monostate>;

Then use the indices trick for generating a compile-time sequence:

auto pushdata_deserialize_helper(int index, const uint8_t *serialized) {
    VariantType result;
    size_t count = 0;
    [&]<std::size_t... Idx>(std::index_sequence<Idx...>) {
        ((index == Idx ? (result.template emplace<Idx>(pushdata_deserialize_impl<DataClassCollection>(serialized)),0) : 0), ...);
    }(std::index_sequence_for<DataClassCollection...>{});
    return result;
}

Demo: https://godbolt.org/z/83aTo41Mf


Another way is to filter out the duplicate types of the std::variant.

I'll use a typelist type as a working type to avoid instantiating a bunch of unnecessary std::variant specializations:

template<class...> struct typelist{};

First a type trait to filter out duplicate types from a typelist (adapted from How to filter duplicate types from tuple C++):

template <typename T, typename... Ts>
struct unique : std::type_identity<T> {};

template <typename... Ts, typename U, typename... Us>
struct unique<typelist<Ts...>, U, Us...>
    : std::conditional_t<(std::is_same_v<U, Ts> || ...)
                       , unique<typelist<Ts...>, Us...>
                       , unique<typelist<Ts..., U>, Us...>> {};

template <typename... Ts>
using unique_typelist = typename unique<typelist<>, Ts...>::type;

Then a helper to apply a given template to a typelist:

template <template<class...>class Temp, class Typelist>
struct apply_typelist_impl{};

template <template<class...>class Temp, class... Args>
struct apply_typelist_impl<Temp, typelist<Args...>> {
    using type = Temp<Args...>;
};

template <template<class...>class Temp, class Typelist>
using apply_typelist = typename apply_typelist_impl<Temp, Typelist>::type;

Then we use these to filter out duplicates from the std::variant type:

using TypesForVariant = 
    unique_typelist<std::monostate, 
                    std::pair<typename DataClassCollection::typeK, typename DataClassCollection::typeV>...>;
using VariantType = apply_typelist<std::variant, TypesForVariant>;

Demo: https://godbolt.org/z/E93Tjadf8

TartanLlama
  • 63,752
  • 13
  • 157
  • 193
  • @user3641187 your comment above gave me an idea of how to do this with duplicated types in the variant, I've added a new version to the answer – TartanLlama Apr 18 '23 at 09:28
  • Is definitely the more readable solution than using the typelist filtering approach – user3641187 Apr 18 '23 at 09:36
  • Although there is something to be said about avoiding duplicate std::variant instantiations so I do appreciate the other approach as well, even though it does look scary. – user3641187 Apr 18 '23 at 09:42
0

An alternative, you might to turn your int index into std::integral_constant (inside a std::variant):

template <std::size_t... Is>
auto getVarIndex(std::index_sequence<Is...>, std::size_t i)
{
    using ResType = std::variant<std::integral_constant<std::size_t, Is>...>;
    using ResTypeGenerator = ResType();
    ResTypeGenerator* funcs[] = { +[]{ return ResType{std::integral_constant<std::size_t, Is>{}}; }... };
    assert(i < sizeof...(Is));
    return funcs[i]();
}

VariantType pushdata(const uint8_t *serialized, uint8_t index){
    VariantType result;
    std::visit([&]<std::size_t I>(std::integral_constant<std::size_t, I>) {
        if constexpr (I != 0) { // not monostate
            using VarAltType = std::variant_alternative_t<I, VariantType>;
            using DataClassType = DataClass<typename VarAltType::first_type, typename VarAltType::second_type>;
            result.template emplace<I>(pushdata_deserialize_impl<DataClassType>(serialized));
        }
               },
               getVarIndex(std::make_index_sequence<1 + sizeof...(DataClassCollection)>(), index));
    return result;
}

Demo

Jarod42
  • 203,559
  • 14
  • 181
  • 302