Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the most compact way to extract the template arguments of a class and iterate over them?

In the little program below, I show the solution I currently use to extract the template arguments of a class and iterate over it via a recursive helper function.

I wonder if there is a more concise way to do it, as I explain in the pseudo-code in the comments below.

template <int...Is> struct Pack {};

template <int I> struct B
{
    static void foo() { std::cout << I << "\n"; }
};

// recursive helper function, also used to extract the parameter pack arguments
template <int I, int...Is>
void foo_helper( Pack<I, Is...>&& )
{
    B<I>::foo();
    foo_helper( Pack<Is...>{} );
}

// terminate recursion
void foo_helper( Pack<>&& ) {}

struct A
{
    typedef Pack<1,3,5> ints;

    static void foo()
    {
        // this is what I do
        foo_helper(ints{});

        // this is what I would like to do, ideally in one single line
        // 1) extract the template arguments pack from ints, without creating an helper function for that
        // 2) iterate on the template arguments of the pack without a recursive helper
        // In pseudocode, something like:
        // (B<IterateOver<ArgumentsOf<ints>>>::foo());
    }
};

int main()
{
    A::foo();
}
like image 503
Fabio Avatar asked Jan 18 '16 10:01

Fabio


3 Answers

If you want to do metaprogramming, start working in types. If you want non-type template parameters, move them over to types asap.

Below, I first take Pack<1,2,3> and convert it to types< std::integral_constant<int, 1>, std::integral_constant<int, 2>, std::integral_constant<int, 3> >. This is a list of types that is in obvious correspondence to your pack of ints.

Then, I introduce a tag type template. This is a type which "carries" another type, but it itself is stateless. You can extract the type from an value of an instance of the template as a bonus.

Third, I write a "for each type" function that takes a lambda and a pack of types, and proceeds to call the lambda once for each of the types, passing in a tag type.

In the body of the lambda, we can extract the passed type by using decltype on the tag variable (or a helper macro).

We chain those together, and from the passed tag type we can extract the integer in the original pack.

The result is you can inject this into your code:

for_each_type( [&](auto tag){
  constexpr int i = TAG_TYPE(tag){};
  // use i
}, ints_as_types_t<ints>{} );

in the middle of your method, and work on the ints "inline".

If we wanted to only solve your specific problem, we'd do a bit less boilerplate, but I like the genericness.


template<class...>struct types{using type=types;};

template <int...Is> struct Pack {};

template<class pack> struct ints_as_types;
template<class pack>
using ints_as_types_t=typename ints_as_types<pack>::type;

template<class T, template<T...>class pack, T...ts>
struct ints_as_types<pack<ts...>> {
  using type=types<std::integral_constant<T,ts>...>;
};

now we can do:

using pack = ints_as_types_t<Pack<1,2,3>>;

and pack is a list of types, not a list of integers.

Now some hana-style metaprogramming: (metaprogramming with values instead of pure types)

template<class T>struct tag_t{using type=T; constexpr tag_t(){};};
template<class T>constexpr tag_t<T> tag={};
template<class Tag>using type_t=typename Tag::type;
#define TAG_TYPE(...) type_t<std::decay_t<decltype(__VA_ARGS__)>>;

template<class F, class...Ts>
void for_each_type(F&& f, types<Ts...>) {
  using discard=int[];
  (void)discard{ 0, ((
    f(tag<Ts>)
  ),void(),0)...};
}

which lets you iterate over a collection of types.

for_each_type( [&](auto tag){
  constexpr int i = TAG_TYPE(tag){};
  // use i
}, ints_as_types_t<ints>{} );

gives you a lambda that has a constexpr int i for each of the types in your list.

A bunch of the above work lifts your list of ints into a list of types, because working with only types makes metaprogramming less special-case. You can skip that lifting, and write a for_each_integer that takes a Pack<int...> directly with less code, but it seems less useful to me.

like image 140
Yakk - Adam Nevraumont Avatar answered Oct 19 '22 20:10

Yakk - Adam Nevraumont


You could add a foo_for_each function to Pack:

template <int...Is> struct Pack {    
    template <template <int> class T>
    static void foo_for_each () {
        std::initializer_list<int> { (T<Is>::foo(),0)... } ;
    }
};

Then you would just write:

ints::foo_for_each<B>();

This will call B<N>::foo for each N in the pack.

As suggested by Yakk, you could pass in a lambda which gets a tag type as an argument to create a generic Pack::for_each:

template <typename T> struct tag { using type = T; };
template <typename T> using type_t = typename T::type;

template <int...Is> struct Pack {    
    template <template <int> class T, typename Func>
    static void for_each (Func&& func) {
        std::initializer_list<int> { 
          ((std::forward<Func>(func)(tag<T<Is>>{}))  0)... 
        } ;
    }
};

Then you could call like this:

auto call_foo = [](auto tag) { type_t<decltype(tag)>::foo(); };
ints::for_each<B>(call_foo);
like image 2
TartanLlama Avatar answered Oct 19 '22 21:10

TartanLlama


This is the shortest I can come up with:

#include <iostream>

template<int... Is>
struct Pack;

template <int I> struct B
{
    static void foo() { std::cout << I << "\n"; }
};

template<typename PACK> struct unpack;

template<int...Is>
struct unpack<Pack<Is...>>
{ 
  template<template<int> class T>
  static void call()
  { 
    using swallow = int[sizeof...(Is)];
    (void) swallow{(T<Is>::foo(), 0)...};
  }
};

struct A
{
    typedef Pack<1,3,5> ints;

    static void foo()
    {
      unpack<ints>::call<B>();
    }
};

int main()
{
    A::foo();
}
like image 1
Rumburak Avatar answered Oct 19 '22 19:10

Rumburak