如何使用任意参数作为模板参数的指针?

时间:2013-05-26 15:25:54

标签: c++ c++11 callback variadic-templates variadic-functions

这是我过去几天一直在研究的语义优化问题,我被困住了。我的真实程序运行在RTOS(特别是FreeRTOS)上,我需要生成任务(这是简单的,非终止版本的线程)。 C API为任务的入口点采用void (*)(void*),并为void*参数。非常标准的票价。

我为一个任务编写了一个包装类,而不是做一个老式的实现,比如有一个必须被最终任务类覆盖的虚方法,我宁愿让C ++生成必要的参数存储对象和胶合函数通过可变参数模板和函数。

我已经使用lambdas和std::function以及std::bind完成了这项工作,但它们似乎实现了一些膨胀,即直到运行时才解析函数目标。基本上与虚拟方法相同的机制将使用。如果可能的话,我正试图减少所有开销。与硬编码实现相比,每个实例的膨胀大约为200字节。 (这是在ARM Cortex-M3上,总闪存为128K,我们只剩下大约500个字节。)我在主题上找到的所有SO问题同样推迟了函数的解析直到运行时。

我们的想法是代码:

  1. 将可变参数的衰减版本存储在堆上分配的对象中(这是一种简化;可以使用分配器),并将其作为void*参数传递,
  2. 将生成的调用岛函数作为入口点,使用签名void(void*),使用存储的参数调用目标函数,并
  3. (这是我无法弄清楚的部分)让编译器从目标函数的签名中推断出参数列表的类型,遵循不要重复自己原则。
  4. 请注意函数指针及其参数类型是已知的并在编译时解析,并且传递给函数的实际参数值在运行时才知道(因为它们包括对象指针和运行时配置选项等内容。
  5. 在下面的示例中,当我更愿意写Task<void (*)(int), bar, int> task_bar(100);Task<bar> task_bar(100); 并且编译器数字时,我必须将其中一个任务实例化为Task task_bar<bar>(100); out(或以某种方式在库中告诉它)可变参数必须与指定函数的参数列表匹配。

    “显而易见”的答案是某种模板签名,如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 个答案:

答案 0 :(得分:4)

开始使用一些元编程样板:

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样板的宏。在C ++ 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*"
}

Live version

而且,对于子孙后代,我的第一次刺,这很有趣,但错过了标记: Old version(它执行函数的运行时绑定,导致调用voidary函数不可避免地执行两次调用)

一个小问题 - 如果你没有std::move任务中的参数(或以其他方式在该调用中引发move,比如使用临时代码),你最终会得到引用它们而不是void*中的副本。