3

Suppose we have

template <unsigned N> foo() { /* ... */ }

defined. Now, I want to implement

do_foo(unsigned n);

which calls the corresponding variant of foo(). This is not merely a synthetic example - this does actually happen in real life (of course, not necessarily with void-to-void functions and just one template parameter, but I'm simplfying. Of course, in C++, we can't have the following:

do_foo(unsigned n) { foo<n>(); }

and what I do right now is

do_foo(unsigned n) { 
    switch(n) {    
    case n_1: foo<n_1>(); break;
    case n_2: foo<n_2>(); break;
    /* ... */
    case n_k: foo<n_k>(); break;
    }
}

when I know n is effectively limited in range to n_1,...,n_k. But this is unseemly, and much more so when the call is longer and I need to duplicate a long sequence of template and regular parameters many times.

I was about to start working on a macro to produce these switch statements, when I got to thinking maybe someone has already worked on this in some library and could share what they did. If not, perhaps it's still feasible to have some kind of C++ construct which takes an arbitrary function, with any sequence of template and non-template parameters including some numeric template parameter, and a sequence of values in some form, to produce a wrapper which can take that template parameter as an additional run-time parameter instead, e.g.

auto& transformed_foo = magic<decltype(foo)>(foo)::transformed;
einpoklum
  • 118,144
  • 57
  • 340
  • 684
  • Is anything stopping you from just making it a `constexpr` function? Like [this](http://coliru.stacked-crooked.com/a/bf29703b8f9511db). – TartanLlama Aug 12 '16 at 10:29
  • @TartanLlama: You're thinking of the wrong direction for my question. I don't want an extra parameter to use at _compile-time_, I want more flexibility at run-time with a function which compiles very differently based on the value of N. – einpoklum Aug 12 '16 at 11:28
  • Cool, so I guess you have a bunch of specializations which prevent you from simply making it `constexpr`. You can probably do something similar to tricks for indexing `std::tuple`s at runtime. See [this](http://stackoverflow.com/questions/28997271/c11-way-to-index-tuple-at-runtime-without-using-switch) for example. I'll try and write up a solution if I have time. – TartanLlama Aug 12 '16 at 11:33
  • @TartanLlama: Please don't make guesses... I want something general. Plus, maybe it's not even my code and `foo()` is a library function. – einpoklum Aug 12 '16 at 12:59
  • 1
    Something like [this](http://coliru.stacked-crooked.com/a/75a536541aa0ffa1)? – TartanLlama Aug 12 '16 at 13:16
  • 1
    @TartanLlama: Now you're talking... an index sequence and ellipsis expansion. Nice. But maybe you should make [this](http://coliru.stacked-crooked.com/a/d60df0e07818976e) the example to emphasize how the parameters cannot be determined at compile time. – einpoklum Aug 12 '16 at 16:12

3 Answers3

1

To make this easier, I'll make a functor wrapper around foo:

struct Foo {
    template <unsigned N>
    void operator()(std::integral_constant<unsigned,N>)
    { foo<N>(); }
};

Now we can sketch out our visitor:

template <std::size_t Start, std::size_t End, typename F>
void visit(F f, std::size_t n) {
    //magic
};

When it's finished, it'll get called like this:

visit<0, 10>(Foo{}, i);
// min^  ^max       

The magic is going to involve using the indices trick. We'll generate an index sequence covering the range desired and tag-dispatch to a helper:

visit<Start>(f, n, std::make_index_sequence<End-Start>{});

Now the real meat of the implementation. We'll build up an array of std::functions, then index it with the runtime-supplied value:

template <std::size_t Offset, std::size_t... Idx, typename F>
void visit(F f, std::size_t n, std::index_sequence<Idx...>) {
    std::array<std::function<void()>, sizeof...(Idx)> funcs {{
        [&f](){f(std::integral_constant<unsigned,Idx+Offset>{});}...
    }};

    funcs[n - Offset]();
};

This could certainly be made more generic, but this should give you a good starting point to apply to your problem domain.

Live Demo

T.C.
  • 133,968
  • 17
  • 288
  • 421
TartanLlama
  • 63,752
  • 13
  • 157
  • 193
  • 1
    This is great. But - 1. I can't be the first person to want to do this. Isn't such an idiom part of some library? Some TS or somewhere in Boost? 2. Why is the object wrapper necessary? Can't function pointers be used? – einpoklum Aug 12 '16 at 19:21
  • 1
    @einpoklum 1. Perhaps, but I'm not aware of one. 2. The problem with using function pointers for this is that you need to resolve which function your pointing to at the call site of `visit`, so it doesn't really work. You could likely come up with a way to use a generic lambda in C++14. – TartanLlama Aug 12 '16 at 19:32
  • I was just trying to use this - and noticed it doesn't compile with GCC 6.x nor with 7.x (!!) It compiles with clang 6.0 (and possibly earlier), and with GCC 8.x. Also,this code is C++11 (except for the missing `std::index_sequence<>` which you would need to replace with your own). – einpoklum Nov 07 '18 at 15:40
  • 1
    There's also no real reason to use `std::function` here. – T.C. Nov 07 '18 at 20:42
1

This is an extension of @TartanLlama's solution for a no-argument function to a function with an arbitrary number of arguments. It also has the added benefit of circumventing a GCC bug (before version 8) of failing to properly expand variadic template parameter packs when the expansion is of a lambda.

#include <iostream>
#include <utility>
#include <array>
#include <functional>

struct Foo {
    template <std::size_t N, typename... Ts> void operator()(std::integral_constant<std::size_t,N>, Ts... args)
    { foo<N>(std::forward<Ts>(args)...); }
};

template <std::size_t N, typename F, typename... Ts>
std::function<void(Ts...)> make_visitor(F f) {
    return 
        [&f](Ts... args) {
            f(std::integral_constant<std::size_t,N>{}, std::forward<Ts>(args)...);
        };
}

template <std::size_t Offset, std::size_t... Idx, typename F, typename... Ts>
void visit(F f, std::index_sequence<Idx...>, std::size_t n, Ts... args) {
    static std::array<std::function<void(Ts...)>, sizeof...(Idx)> funcs {{
        make_visitor<Idx+Offset, F, Ts...>(f)...
    }};
    funcs[n-Offset](std::forward<Ts>(args)...);
};

template <std::size_t Start, std::size_t End, typename F, typename... Ts>
void visit(F f, std::size_t n, Ts... args) {
    visit<Start>(f, std::make_index_sequence<End-Start>{}, n, std::forward<Ts>(args)...);
};

Live demo

einpoklum
  • 118,144
  • 57
  • 340
  • 684
1

While the other two answers are fairly generic, they are a bit hard for the compiler to optimise. I currently in a very similar situation use the following solution:

#include <utility>
template<std::size_t x>
int tf() { return x; }

template<std::size_t... choices>
std::size_t caller_of_tf_impl(std::size_t y, std::index_sequence<choices...>) {
  std::size_t z = 42;
  ( void( choices == y && (z = tf<choices>(), true) ), ...);
  return z;
}

template<std::size_t max_x, typename Choices = std::make_index_sequence<max_x> >
std::size_t caller_of_tf(std::size_t y) {
  return caller_of_tf_impl(y, Choices{});
}

int a(int x) {
  constexpr std::size_t max_value = 15;
  return caller_of_tf<max_value+1>(x);
}

where we have some templated function tf which for illustrative reasons simply returns its template argument and a function caller_of_tf(y) which wants to call the appropriate tf<X> given a run-time argument y. It essentially relies on first constructing an appropriately-sized argument pack and then expanding this argument pack using a short-circuiting && operator which strictly only evaluates its second argument if the first argument is true. We then simply compare the run-time parameter to each element of the parameter pack.

The nice thing about this solution is that it is straightforward to optimise, e.g. Clang turns a() above into a check that x is smaller than 16 and returns that. GCC is slightly less optimal but still manages to only use an if-else chain. Doing the same with the solution posted by einpoklum results in a lot more assembly being generated (e.g. with GCC). The downside, of course, is the solution above is more specific.

Claudius
  • 550
  • 3
  • 14