Идиома для имитации параметров числового шаблона во время выполнения?

Предположим, у нас есть

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

определены. Теперь я хочу реализовать

do_foo(unsigned n);

который вызывает соответствующий вариант foo(), Это не просто синтетический пример - это действительно происходит в реальной жизни (конечно, не обязательно с функциями void-to-void и только одним параметром шаблона, но я упрощаю. Конечно, в C++ мы не можем иметь следующее:

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

и что я делаю сейчас

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;
    }
}

когда я знаю, что n эффективно ограничен в диапазоне до n_1,...,n_k. Но это неправдоподобно, и тем более, когда вызов длиннее, и мне нужно многократно дублировать длинную последовательность шаблонов и обычных параметров.

Я собирался начать работать над макросом для создания этих операторов switch, когда подумал, может быть, кто-то уже работал над этим в какой-то библиотеке и может поделиться тем, что он сделал. Если нет, возможно, все еще возможно иметь некоторую конструкцию C++, которая принимает произвольную функцию, с любой последовательностью параметров шаблона и не шаблонов, включая некоторый числовой параметр шаблона и последовательность значений в некоторой форме, для создания оболочки, которая вместо этого можно использовать этот параметр шаблона в качестве дополнительного параметра времени выполнения, например

auto& transformed_foo = magic<decltype(foo)>(foo)::transformed;

2 ответа

Это расширение решения @TartanLlama для функции без аргументов для функции с произвольным числом аргументов. Он также имеет дополнительное преимущество: он позволяет обойти ошибку GCC (до версии 8), заключающуюся в невозможности правильно развернуть пакеты параметров вариабельного шаблона, когда расширение является лямбда-выражением.

#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)...);
};

Живая демо

Чтобы сделать это проще, я сделаю функтор вокруг foo:

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

Теперь мы можем сделать набросок нашего посетителя:

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

Когда он закончится, он будет вызываться так:

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

Магия будет включать использование трюка с индексами. Мы сгенерируем индексную последовательность, охватывающую требуемый диапазон и отправку тега помощнику:

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

Теперь настоящее мясо реализации. Мы создадим массив std::functions, затем индексируйте его с помощью предоставленного во время выполнения значения:

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]();
};

Это, конечно, можно сделать более общим, но это должно дать вам хорошую отправную точку для применения к вашей проблемной области.

Live Demo

Хотя два других ответа довольно общие, компилятору немного сложно их оптимизировать. В настоящее время я в очень похожей ситуации использую следующее решение:

#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);
}

где у нас есть шаблонная функция tf который для наглядности просто возвращает свой аргумент шаблона и функцию caller_of_tf(y) который хочет назвать соответствующий tf<X> учитывая аргумент времени выполнения y. По сути, он полагается сначала на создание пакета аргументов подходящего размера, а затем на расширение этого набора аргументов с помощью короткого замыкания&&оператор, который строго оценивает свой второй аргумент, только если первый аргумент истинен. Затем мы просто сравниваем параметр времени выполнения с каждым элементом пакета параметров.

Преимущество этого решения в том, что его легко оптимизировать, например, Clang превращаетсяa() выше в проверку, что xменьше 16 и возвращает это значение. GCC немного менее оптимален, но ему все же удается использовать только цепочку if-else. То же самое с решением, опубликованным einpoklum, приводит к созданию намного большего количества сборок (например, с GCC). Обратной стороной, конечно же, является то, что приведенное выше решение является более конкретным.

Другие вопросы по тегам