Vasily
Vasily

Reputation: 255

Call sequence of template function for sequence of template parameters

Let's imagine I have several template functions, e.g.:

template <int I> void f();
template <int I> void g();
template <int I> void h();

How can I call sequence of any of these functions for sequence of template parameters?

In other words, I need such behaviour:

{some template magic}<1, 5>(f); // This is pseudocode, I don't need exactly this format of calling.

unrolls into:

f<1>();
f<2>();
f<3>();
f<4>();
f<5>();

And I need the same method to work for every of my functions (not only for f, but for g and h too) without writing big awkward structure for every of these functions.

I can use C++11, and even already implemented in latest development gcc version C++1y/C++14 functionality (http://gcc.gnu.org/projects/cxx1y.html), e.g. polymorphic lambdas.

Upvotes: 2

Views: 871

Answers (2)

ysdx
ysdx

Reputation: 9325

Using reified functions and template template arguments:

#include <iostream>

template<int I> class f {
public:
  static void call() {
    std::cout << I << '\n';
  }
};

template<template<int I> class X, int I, int J> class magic {
 public:
  static void call() {
    X<I>::call();
    magic::call();
  }
};

template<template<int I> class X, int I> class magic<X,I,I> {
public:
  static void call() {
    X<I>::call();
  }
};

int main(int argc, char** argv) {
  magic<f,2,6>::call();
  return 0;
}

Upvotes: 5

dyp
dyp

Reputation: 39111

With C++1y features. Instead of calling the function directly and passing the template argument as, well, a template argument, you can create a lambda that takes a function argument which contains the template argument as part of its type. I.e.

f<42>();
[](std::integral_constant<int, 42> x) { f<x.value>(); }
[](auto x) { f<x.value>(); }

With this idea, we can pass the function template f around, when wrapped into such a polymorphic lambda. That's possible for any kind of overload set, one of the things you can't do with ordinary lambdas.

To call f with a sequence of template arguments, we'll need the common indices classes for the indices expansion trick. Those will be in the C++1y Standard Library. Coliru's clang++ compiler for example still uses an older libstdc++ which doesn't have them AFAIK. But we can write our own:

#include <utility>

using std::integral_constant;
using std::integer_sequence;       // C++1y StdLib
using std::make_integer_sequence;  // C++1y StdLib
// C++11 implementation of those two C++1y StdLib classes:
/*
template<class T, int...> struct integer_sequence {};
template<class T, int N, int... Is>
struct make_integer_sequence : make_integer_sequence<T, N-1, N-1, Is...> {};
template<class T, int... Is>
struct make_integer_sequence<T, 0, Is...> : integer_sequence<T, Is...> {};
*/

When we write make_integer_sequence<int, 5>, we'll get a type that's derived from integer_sequence<int, 0, 1, 2, 3, 4>. From the latter type, we can deduce the indices:

template<int... Indices> void example(integer_sequence<int, Indices...>);

Inside this function, we have access to the indices as a parameter pack. We'll use the indices to call the lamba / function object f as follows (not the function template f from the question):

f( integral_constant<int, Indices>{} )...
// i.e.
f( integral_constant<int, 0>{} ),
f( integral_constant<int, 1>{} ),
f( integral_constant<int, 2>{} ),
// and so on

Parameter packs can only be expanded in certain contexts. Typically, you'd expand the pack as initializers (e.g. of a dummy array), as the evaluation of those is are guaranteed to be ordered (thanks, Johannes Schaub). Instead of an array, one could use a class type such as

struct expand { constexpr expand(...) {} };
// usage:
expand { pattern... };

A dummy array looks like this:

int expand[] = { pattern... };
(void)expand; // silence compiler warning: `expand` not used

Another tricky part is to deal with functions returning void as the pattern. If we combine a function call with a comma operator, we always get a result

(f(argument), 0) // always has type int and value 0

To break any existing overloaded comma operators, add a void()

(f(argument), void(), 0)

Finally, combine all the above to create magic:

template<int beg, class F, int... Is>
constexpr void magic(F f, integer_sequence<int, Is...>)
{
    int expand[] = { (f(integral_constant<int, beg+Is>{}), void(), 0)... };
    (void)expand;
}

template<int beg, int end, class F>
constexpr auto magic(F f)
{
    //                                              v~~~~~~~v see below (*)
    return magic<beg>(f, make_integer_sequence<int, end-beg+1>{});
}

Usage example:

#include <iostream>
template<int N> void f() { std::cout << N << "\n"; }

int main()
{
    //magic<1, 5>( [](auto x) { f<decltype(x)::value>(); } );
    magic<1, 5>( [](auto x) { f<x.value>(); } );
}

(*) IMHO end-beg+1 is bad practice. There's a reason why the StdLib works with half-open ranges of the form [begin, end): The empty range simply is [begin, begin). With the StdLib using half-open ranges, it might be inconsistent to use closed ranges here. (There's one exception in the StdLib I know of, it has to do with PRNGs and the maximum integer value.)

I'd suggest you'd design your magic interface to take half-open ranges, i.e.

magic<1, 6>( [](auto x) { f<x.value>(); } ); // [1, 6) i.e. {1,2,3,4,5}

with the implementation

template<int beg, int end, class F>
constexpr auto magic(F f)
{
    //                                              v~~~~~v
    return magic<beg>(f, make_integer_sequence<int, end-beg>{});
}

Note the weird +1 disappears.

Upvotes: 7

Related Questions