Идиома для имитации параметров числового шаблона во время выполнения?
Предположим, у нас есть
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]();
};
Это, конечно, можно сделать более общим, но это должно дать вам хорошую отправную точку для применения к вашей проблемной области.
Хотя два других ответа довольно общие, компилятору немного сложно их оптимизировать. В настоящее время я в очень похожей ситуации использую следующее решение:
#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). Обратной стороной, конечно же, является то, что приведенное выше решение является более конкретным.