12

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

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

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

这个想法是让代码:

  1. 将可变参数的衰减版本存储在堆上分配的对象中(这是一种简化;可以使用分配器代替),并将其作为void*参数传递,
  2. 传递一个生成的调用岛函数作为入口点,带有签名void(void*),使用存储的参数调用目标函数,以及
  3. (这是我无法弄清楚的部分)让编译器从目标函数的签名中推断出参数列表的类型,以遵循Don't Repeat Yourself原则。
  4. 请注意,函数指针及其参数类型在编译时是已知的和解析的,传递给函数的实际参数值直到运行时才知道(因为它们包括对象指针和运行时配置选项等内容)。

在下面的示例中,我必须实例化其中一项任务,因为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第一个论点where 的情况也不是。

我不确定这是否可能,所以我在这里问你们想出了什么。为了简化问题,我省略了针对对象方法而不是静态函数的变体代码。

下面是一个有代表性的测试用例。我正在用 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;
}
4

1 回答 1

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*"
}

现场版

而且,为了后代,我的第一次刺,这很有趣,但错过了标记: 旧版本(它对要调用的函数进行运行时绑定,导致对voidary函数的调用不可避免地进行两次调用)

一个小问题——如果你没有std::move在任务中加入参数(或者move在那个调用中引入 a,比如使用临时对象),你最终会得到对它们的引用,而不是在void*.

于 2013-05-26T23:25:37.780 回答