Как иметь указатель на функцию с произвольными аргументами в качестве параметра шаблона?

Это проблема семантической оптимизации, над которой я работаю последние пару дней, и я застрял. Моя настоящая программа работает на 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*.