Как иметь указатель на функцию с произвольными аргументами в качестве параметра шаблона?
Это проблема семантической оптимизации, над которой я работаю последние пару дней, и я застрял. Моя настоящая программа работает на RTOS (FreeRTOS, в частности), и мне нужно запускать задачи (которые являются упрощенными, не заканчивающимися версиями потоков). C API принимает void (*)(void*)
для точки ввода задачи и параметр void*
. Довольно стандартный тариф.
Я написал класс-оболочку для задачи и вместо того, чтобы выполнять одну из реализаций старой школы, например, иметь виртуальный метод, который должен быть переопределен конечным классом задачи, я бы предпочел получить С++ для создания необходимого объект-хранилище параметров и функции клея с помощью вариативных шаблонов и функций.
Я уже делал это с lambdas и std::function
и std::bind
, но они, похоже, реализуют некоторое раздувание, а именно, не разрешая целевую функцию до времени выполнения. В принципе, будет использоваться тот же механизм, что и виртуальный метод. Я стараюсь, если возможно, вырезать все накладные расходы. Расцветка выходила примерно на 200 байт на экземпляр больше, чем жестко закодированная реализация. (Это находится на ARM Cortex-M3 со 128-кратной полной вспышкой, и осталось всего около 500 байт.) Все вопросы SO, которые я нашел в этой теме, аналогично откладывают разрешение функции до времени выполнения.
Идея состоит в том, чтобы код:
- Храните разложившиеся версии вариационных аргументов в объекте, выделенном в куче (это упрощение, вместо этого можно использовать Allocator) и передать это как параметр
void*
,
- Передайте созданную функцию вызова-острова в качестве точки входа с сигнатурой
void(void*)
, которая вызывает целевую функцию с сохраненными параметрами и
- (Это часть, которую я не могу понять), чтобы компилятор выводил типы списка аргументов из сигнатуры целевой функции, чтобы следовать принципу "Не повторять себя".
- Обратите внимание, что указатель функции и ее типы аргументов известны и разрешены во время компиляции, а фактические значения аргумента , переданные функции, неизвестны до времени выполнения (потому что они включают такие вещи, как указатели объектов и параметры конфигурации времени исполнения).
В приведенном ниже примере мне нужно создать экземпляр одной из заданий как Task<void (*)(int), bar, int> task_bar(100);
, когда я бы предпочел написать Task<bar> task_bar(100);
или Task task_bar<bar>(100);
и выяснить, компилятор (или как-то сказать) в библиотеке), что переменные аргументы должны соответствовать списку аргументов указанной функции.
"Очевидным" ответом будет какая-то подпись шаблона, например template<typename... Args, void (*Function)(Args...)>
, но, разумеется, не компилируется. Не так же, как Function
- первый аргумент.
Я не уверен, что это возможно, поэтому я прошу здесь посмотреть, что вы, ребята, придумали. Я пропустил вариантный код, который предназначен для объектных методов вместо статических функций, чтобы упростить вопрос.
Ниже приведен репрезентативный тестовый пример. Я создаю его с помощью gcc 4.7.3 и флага -std=gnu++11
.
#include <utility>
#include <iostream>
using namespace std;
void foo() { cout << "foo()\n"; }
void bar(int val) { cout << "bar(" << val << ")\n"; }
template<typename Callable, Callable Target, typename... Args>
struct TaskArgs;
template<typename Callable, Callable Target>
struct TaskArgs<Callable, Target> {
constexpr TaskArgs() {}
template<typename... Args>
void CallFunction(Args&&... args) const
{ Target(std::forward<Args>(args)...); }
};
template<typename Callable, Callable Target, typename ThisArg,
typename... Args>
struct TaskArgs<Callable, Target, ThisArg, Args...> {
typename std::decay<ThisArg>::type arg;
TaskArgs<Callable, Target, Args...> sub;
constexpr TaskArgs(ThisArg&& arg_, Args&&... remain)
: arg(arg_), sub(std::forward<Args>(remain)...) {}
template<typename... CurrentArgs>
void CallFunction(CurrentArgs&&... args) const
{ sub.CallFunction(std::forward<CurrentArgs>(args)..., arg); }
};
template<typename Callable, Callable Target, typename... Args>
struct TaskFunction {
TaskArgs<Callable, Target, Args...> args;
constexpr TaskFunction(Args&&... args_)
: args(std::forward<Args>(args_)...) {}
void operator()() const { args.CallFunction(); }
};
// Would really rather template the constructor instead of the whole class.
// Nothing else in the class is virtual, either.
template<typename Callable, Callable Entry, typename... Args>
class Task {
public:
typedef TaskFunction<Callable, Entry, Args...> Function;
Task(Args&&... args): taskEntryPoint(&Exec<Function>),
taskParam(new Function(std::forward<Args>(args)...)) { Run(); }
template<typename Target>
static void Exec(void* param) { (*static_cast<Target*>(param))(); }
// RTOS actually calls something like Run() from within the new task.
void Run() { (*taskEntryPoint)(taskParam); }
private:
// RTOS actually stores these.
void (*taskEntryPoint)(void*);
void* taskParam;
};
int main()
{
Task<void (*)(), foo> task_foo;
Task<void (*)(int), bar, int> task_bar(100);
return 0;
}
Ответы
Ответ 1
Некоторые метапрограммирующие шаблоны для запуска:
template<int...> struct seq {};
template<int Min, int Max, int... s> struct make_seq:make_seq<Min, Max-1, Max-1, s...> {};
template<int Min, int... s> struct make_seq<Min, Min, s...> {
typedef seq<s...> type;
};
template<int Max, int Min=0>
using MakeSeq = typename make_seq<Min, Max>::type;
Помощник для распаковки кортежа:
#include <tuple>
template<typename Func, Func f, typename Tuple, int... s>
void do_call( seq<s...>, Tuple&& tup ) {
f( std::get<s>(tup)... );
}
Тип результирующего указателя функции:
typedef void(*pvoidary)(void*);
Фактическая рабочая лошадка. Обратите внимание, что накладные расходы виртуальной функции не происходят:
template<typename FuncType, FuncType Func, typename... Args>
std::tuple<pvoidary, std::tuple<Args...>*> make_task( Args&&... args ) {
typedef std::tuple<Args...> pack;
pack* pvoid = new pack( std::forward<Args>(args)... );
return std::make_tuple(
[](void* pdata)->void {
pack* ppack = reinterpret_cast<pack*>(pdata);
do_call<FuncType, Func>( MakeSeq<sizeof...(Args)>(), *ppack );
},
pvoid
);
}
Вот макрос, который удаляет шаблон decltype
. В С++ 17 (и, возможно, 14) это не обязательно, мы можем вывести первый аргумент из второго:
#define MAKE_TASK( FUNC ) make_task< typename std::decay<decltype(FUNC)>::type, FUNC >
Жгут проводов:
#include <iostream>
void test( int x ) {
std::cout << "X:" << x << "\n";
}
void test2( std::string s ) {
std::cout << "S:" << s.c_str() << "\n";
}
int main() {
auto task = MAKE_TASK(test)( 7 );
pvoidary pFunc;
void* pVoid;
std::tie(pFunc, pVoid) = task;
pFunc(pVoid);
delete std::get<1>(task); // cleanup of the "void*"
auto task2 = MAKE_TASK(test2)("hello");
std::tie(pFunc, pVoid) = task2;
pFunc(pVoid);
delete std::get<1>(task2); // cleanup of the "void*"
}
Текущая версия
И, для потомков, мой первый удар, который является забавой, но пропустил отметку:
Старая версия (она выполняет привязку функции к вызову во время выполнения, что приводит к вызовам функции voidary
, выполняющей два вызова неизбежно)
Одна незначительная ошибка - если вы не используете std::move
аргументы в задаче (или иначе вызываете move
в этом вызове, например, используя временные ресурсы), вы получите ссылки на них, а не копии из них в void*
.