1

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.

A M
  • 14,694
  • 5
  • 19
  • 44
  • Aside: why *don't* you want a `tuple>` for the case `Odometer`? It seems like `IsTuple` is a solution looking for a problem – Caleth Feb 20 '23 at 09:43
  • `tuple>`would have only one disk. If at all, then It would need a 2d vector. But then I would have the same problem. – A M Feb 20 '23 at 09:45
  • That's a really strange requirement. I'd be tempted to split that out into a separate class, so that you didn't have to deal with a parameter pack. Or at least make that case a partial specialisation – Caleth Feb 20 '23 at 15:44

3 Answers3

1

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

康桓瑋
  • 33,481
  • 5
  • 40
  • 90
1

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.

Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • 1
    This does not work for the use case, where I want 1 template parameter like `int` and then have several disks for those `int` values. Example: `Odometer odo1(1, std::vector{ 2,3 }, std::list{ 4, 5, 6 });` will not compile. Or, I made a mistake . . . . – A M Feb 20 '23 at 15:47
  • @Peter [here](https://godbolt.org/z/nhW9qbvY5) is a fixed version – Caleth Feb 20 '23 at 15:50
1

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))...} {}
};
Caleth
  • 52,200
  • 2
  • 44
  • 75