Idiom for simulating run-time numeric template parameters?

安稳与你 提交于 2020-01-28 02:29:16

问题


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;

回答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




回答2:


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.




回答3:


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



来源:https://stackoverflow.com/questions/38914655/idiom-for-simulating-run-time-numeric-template-parameters

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!