Iterate over 2 template parameter packs in parallel

233 Views Asked by At

I started to implement a very flexible Odometer. It may have several disks with even different amount of values on each different disk. And, as an extension, even the data type of values on each of the single disks could be different.

All this shall be implemented with one class. The number of template parameters defines the behavior of the class.

  • 1 template parameter: Like Odomoter<int> shall result in an Odometer having int values on each disk. The resulting internal data type will be a std::vector<std::vector<int>>
  • 2 or more template parameters: The number of template parameter will define the number of single disks of the Odometer. Each disk has the data type of the template parameter. In the case of Odometer<char, int, double>, this will result in a data type std::tuple<std::vector<char>, std::vector<int>, std::vector<double>>

Now, I want to add a variadic constructor, where I can add whatever data. Of course types and number of arguments must match. I omit the check for the moment and will add it later.

So, now I have a templatized variadic class and a variadic constructor. So, I have the parameter pack off the class and the parameter pack of the constructor.

Now I would need iterate over the elements of both parameter packs at the same time in parallel.

Please see the below code example for an illustration of the problem (I deleted most of the code in the class, to just show you the problem):

#include <vector>
#include <tuple>
#include <list>
#include <initializer_list>

template<typename...Ts> 
struct Odometer {

    static constexpr bool IsTuple = ((std::tuple_size<std::tuple<Ts...>>::value) > 1);

    template<typename...Ts>
    using Tuples = std::tuple<std::vector<Ts>...>;

    template<typename...Ts>
    using MyType = std::tuple_element_t<0, std::tuple<Ts...>>;

    template<typename...Ts>
    using Vectors = std::vector<std::vector<MyType<Ts...>>>;

    template<typename...Ts>
    using Disks = std::conditional<IsTuple, Tuples<Ts...>, Vectors<Ts...>>::type;

    Disks<Ts...> disks{};

    template <typename...Args>
    Odometer(Args...args) {

        if constexpr (IsTuple) {
            // Here disk is a std::tuple<std::vector<char>, std::vector<int>, std::vector<double>>
            ([&] {
            //std::vector<MyType<Ts...>> disk{};   // Does not work. Or would always be a std::vector<char>
            if constexpr (std::ranges::range<Args>) {
                //for (const auto& r : args)       // Does not work
                    //disk.push_back(r);           // Does not work
            }
            else {
                //disk.push_back(args); // Does not work
            } } (), ...);
        }
        else {
            ([&] {
                disks.push_back({});
                if constexpr (std::ranges::range<Args>) {
                    for (const auto& r : args)
                        disks.back().push_back(r);
                }
                else {
                    disks.back().push_back(args);
                } } (), ...);
        }
    }
};
int main() {
    
    Odometer<char, int, double> odo2('a', std::vector{1,2,3}, std::list{4.4, 5.5});
}

I can iterate over the parameter pack of the constructor using a fold expression. I could also use std::apply. But, I need to iterate also over the tuple elements of the "disks", defined by the class template parameters.

I do not want to use recursive templates.


So, I need to iterate of 2 parameter packs in parallel at the same time. How could this be done?


The only idea I have now is to use a helper class with a std::index_sequence, but I do not know.

Please be reminded. Check of number of elements in parameter packs and type will be done later.

3

There are 3 best solutions below

1
康桓瑋 On BEST ANSWER

The only idea I have now is to use a helper class with a std::index_sequence, but I do not know.

You can use template lambda to expand index_sequence and get the corresponding tuple elements through std::get<Is>:

static_assert(sizeof...(Args) == sizeof...(Ts));
[&]<std::size_t... Is>(std::index_sequence<Is...>) {
  ([&] {
    auto& disk = std::get<Is>(disks);
    if constexpr (std::ranges::range<Args>)
      for (const auto& elem : args)
        disk.push_back(elem);
    else
      disk.push_back(args);
  } (), ...);
}(std::index_sequence_for<Args...>{});

Demo with reduced examples

2
Jarod42 On

You don't need to iterate manually, you just need a function to convert each argument into vector:

template <typename T, typename Arg>
std::vector<T> as_vector(Arg&& arg)
{
    if constexpr (std::ranges::range<std::decay_t<Arg>>) {
        return {std::begin(arg), std::end(arg)};
    } else {
        return {std::forward<Arg>(arg)};
    }
}

and then your constructor is simply (Should be more complicated: SFINAE for nearly copy constructor with forwarding reference to avoid current copies):

template <typename...Args>
Odometer(Args... args) : disks{as_vector<Ts>(args)...} {}

Demo

Notice that as_vector<Ts>(args)... uses both packs with a single ..., so they should have same size.

1
Caleth On

Because you are handling the case where Ts... is a pack of one differently, it would be sensible to make that a specialisation.

Then you can expand both packs in one ... for the general case, and you only have one pack for the special case.

template <typename T, typename Arg>
std::vector<T> as_vector(Arg&& arg)
{
    if constexpr (std::ranges::range<std::decay_t<Arg>>) {
        return {std::begin(arg), std::end(arg)};
    } else {
        return {std::forward<Arg>(arg)};
    }
}

template<typename...Ts>
struct Odometer {
    std::tuple<std::vector<Ts>...> disks{};

    template <typename...Args>
    requires (sizeof...(Args) == sizeof...(Ts))
    Odometer(Args&&... args) : disks{as_vector<Ts>(std::forward<Args>(args))...} {}
};

template<typename T>
struct Odometer<T> {
    std::vector<std::vector<T>> disks{};

    template <typename...Args>
    Odometer(Args&&... args) : disks{as_vector<T>(std::forward<Args>(args))...} {}
};