19

This below code from user Faheem Mitha, is based on user Johannes Schaub - litb's answer in this SO. This code perfectly does what I seek, which is conversion of a tuple into parameter pack, but I don't understand this code well enough and therefore I thought I will create a new discussion that might help template metaprogramming newbies like me. So, please pardon the duplicate posting.

Now moving onto the code

#include <tuple>
#include <iostream>
using std::cout;
using std::endl;

template<int ...> struct seq {};

template<int N, int ...S> struct gens : gens<N - 1, N - 1, S...> { };

template<int ...S> struct gens<0, S...>{ typedef seq<S...> type; };

double foo(int x, float y, double z)
{
    return x + y + z;
}

template <typename ...Args>
struct save_it_for_later
{
    std::tuple<Args...> params;
    double(*func)(Args...);

    double delayed_dispatch()
    {
        return callFunc(typename gens<sizeof...(Args)>::type()); // Item #1
    }

    template<int ...S>
    double callFunc(seq<S...>)
    {
        return func(std::get<S>(params) ...);
    }
};

int main(void)
{
    std::tuple<int, float, double> t = std::make_tuple(1, (float)1.2, 5);
    save_it_for_later<int, float, double> saved = { t, foo };
    cout << saved.delayed_dispatch() << endl;
    return 0;
}

I'm completely confounded by Item #1 above:

  • What purpose does typename serve on that line?
  • I understand that gens<sizeof...(Args)>::type() will expand to gens<3>::type(), but that doesn't seem to match neither template<int N, int ...S> struct gens : gens<N - 1, N - 1, S...> { }; nor template<int ...S> struct gens<0, S...>. I'm obviously missing the point and I'd be glad if someone can explain what is happening here.

I do understand that callFunc gets invoked in this form callFunc(seq<0,1,2>) and the return statement of this method itself expands to return func(std::get<0>(params), std::get<1>(params), std::get<2>(params) and this is what makes this scheme work, but I cannot workout how this seq<0,1,2> type is generated.

Note: Using std::index_sequence_for is not an option, my compiler doesn't support C++14 features.

PS: Can this technique be classified as template metaprogramming?

Community
  • 1
  • 1
DigitalEye
  • 1,456
  • 3
  • 17
  • 26
  • 1
    1) To inform the compiler that `gens::type` is a type, and not, say, a member function. 2) parameter packs can be empty. – T.C. Apr 14 '16 at 02:14
  • 1
    `gens<3>` does too match `template struct gens`, with `N==3` and `S` being an empty pack. – Igor Tandetnik Apr 14 '16 at 02:23
  • 2
    Basically, `seq` is [`std::index_sequence`](http://en.cppreference.com/w/cpp/utility/integer_sequence) and `gens` is `std::make_index_sequence` – Igor Tandetnik Apr 14 '16 at 02:26
  • @T.C. Why would the compiler need that hint? I mean how can it possibly confuse the expression `gens::type()` for anything but a `seq()` constructor call ? – DigitalEye Apr 14 '16 at 14:37
  • When you can use _C++17_ this should collapse to `std::apply()` from ``. – simon.watts Apr 27 '20 at 15:28

2 Answers2

27

Let's look at what happens here:

template<int N, int ...S> struct gens : gens<N - 1, N - 1, S...> { };

template<int ...S> struct gens<0, S...>{ typedef seq<S...> type; };

The first one is a generic template, the second one is a specialization that applies when the first template parameter is 0.

Now, take a piece of paper and pencil, and write down how

 gens<3>

gets defined by the above template. If your answer was:

 struct gens<3> : public gens<2, 2>

then you were right. That's how the first template gets expanded when N is "3", and ...S is empty. gens<N - 1, N - 1, S...>, therefore, becomes gens<2, 2>.

Now, let's keep going, and see how gens<2, 2> gets defined:

 struct gens<2, 2> : public gens<1, 1, 2>

Here, in the template expansion, N is 2, and ...S is "2". Now, let's take the next step, and see how gens<1, 1, 2> is defined:

 struct gens<1, 1, 2> : public gens<0, 0, 1, 2>

Ok, now how does gens<0, 0, 1, 2> gets defined? It can now be defined by the specialization:

 template<int ...S> struct gens<0, S...>{ typedef seq<S...> type; };

So, what happens with struct gens<0, 0, 1, 2> here? Well, in the specialization, "S..." becomes "0, 1, 2", so this becomes, in a manner of speaking:

 struct gens<0, 0, 1, 2> {

   typedef seq<0, 1, 2> type;

 }

Now, keep in mind that all of these publicly inherit from each other, "elephant-style", so:

 gens<3>::type

ends up being a typedef declaration for

 struct seq<0, 1, 2>

And this is used, by the code that follows to convert the tuple into a parameter pack, using another template:

double delayed_dispatch()
{
    return callFunc(typename gens<sizeof...(Args)>::type()); // Item #1
}

...Args are the tuple parameters. So, if there are three elements in the tuple, sizeof(...Args) is 3, and as I've explained above, gens<sizeof...(Args)>::type() becomes gens<3>::type(), a.k.a. seq<0, 1, 2>().

So, now:

template<int ...S>
double callFunc(seq<S...>)
{
    return func(std::get<S>(params) ...);
}

The S... part becomes "0, 1, 2", so the

std::get<S>(params)...

Becomes a parameter pack that gets expanded to:

std::get<0>(params), std::get<1>(params), std::get<2>(params),

And that's how a tuple becomes a parameter pack.

Sam Varshavchik
  • 114,536
  • 5
  • 94
  • 148
3

With C++17 you can use "if constexpr" to create a sequence wrapper:

template <int indxMax, template <int... > class spack, int ... seq>
constexpr auto get_seq17()
{
    static_assert(indxMax >= 0, "Sequence size must be equal to or greater than 0!");
    if constexpr (indxMax > 0)
    {
        typedef decltype(spack<indxMax, seq...>{}) frst;
        constexpr int next = indxMax - 1;
        return get_seq17<next, spack, indxMax, seq...>();
    }
    else
    {
        return spack<indxMax, seq...>{};
    }
}

template <int indxMax, template <int...> class pack>
struct seq_pack
{
    typedef decltype(get_seq17<indxMax, pack>()) seq;
};


//creating a sequence wrapper
template <int ... seq>
struct seqpack {};

//usage
seq_pack<4, seqpack>::seq; //seqpack<0, 1, 2, 3, 4> 

Though this implementation is easier to understand, it is preferable to use std::make_index_sequence<Size> as Julius has mentioned in the comments below.

Sergey Kolesnik
  • 3,009
  • 1
  • 8
  • 28
  • 3
    Technically you could do this, yes. However, since C++14 you should just use `std::index_sequence` and friends. `std::make_index_sequence` can be implemented using O(log(N)) instantiations. Moreover, it can be optimized to benefit from memoization. Special builtin compiler support could also allow O(1) for the `std` tools. You can only make it worse by using your O(N) `constexpr if` solution. Anyway, for understanding the concept (and with the proper warning) your answer is useful. (And it uses `int`, which is great.) – Julius Feb 09 '19 at 12:19