This is a semantic-optimization problem I've been working on over the past couple days, and I'm stuck. My real program runs on a RTOS (FreeRTOS, specifically), and I need to spawn tasks (which are simplified, non-terminating versions of threads). The C API takes a void (*)(void*)
for the task's entry point, and a void*
parameter. Pretty standard fare.
I've written a wrapper class for a task, and rather than doing one of the old-school implementations such as having a virtual method that must be overridden by the final task class, I would rather get C++ to generate the necessary parameter-storage object and glue functions by means of variadic templates and functions.
I've done this with lambdas and std::function
and std::bind
already, but they seem to implement some bloat, namely by not resolving the function target until runtime. Basically the same mechanism as a virtual method would use. I'm trying to cut out all the overhead I can, if possible. The bloat has been coming out to about 200 bytes per instance more than the hard-coded implementation. (This is on an ARM Cortex-M3 with 128K total flash, and we've only got about 500 bytes left.) All of the SO questions I've found on the topic similarly defer resolution of the function until runtime.
The idea is for the code to:
void*
parameter,void(void*)
, that calls the target function with the stored parameters, andIn the example below, I have to instantiate one of the tasks as Task<void (*)(int), bar, int> task_bar(100);
when I would rather write Task<bar> task_bar(100);
or Task task_bar<bar>(100);
and have the compiler figure out (or somehow tell it in the library) that the variadic arguments have to match the argument list of the specified function.
The "obvious" answer would be some kind of template signature like template<typename... Args, void (*Function)(Args...)>
but, needless to say, that does not compile. Nor does the case where Function
is the first argument.
I'm not sure this is even possible, so I'm asking here to see what you guys come up with. I've omitted the variant code that targets object methods instead of static functions in order to simplify the question.
The following is a representative test case. I'm building it with gcc 4.7.3 and the -std=gnu++11
flag.
#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;
}
Some metaprogramming boilerplate to start:
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;
Helper to unpack a tuple:
#include <tuple>
template<typename Func, Func f, typename Tuple, int... s>
void do_call( seq<s...>, Tuple&& tup ) {
f( std::get<s>(tup)... );
}
Type of the resulting function pointer:
typedef void(*pvoidary)(void*);
The actual workhorse. Note that no virtual function overhead occurs:
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
);
}
Here is a macro that removes some decltype
boilerplate. In C++17 (and maybe 14) this shouldn't be required, we can deduce the first argument from the second:
#define MAKE_TASK( FUNC ) make_task< typename std::decay<decltype(FUNC)>::type, FUNC >
Test harness:
#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
And, for posterity, my first stab, which is fun, but missed the mark:
Old version (It does run-time binding of the function to call, resulting in calls to the voidary
function doing two calls unavoidably)
One minor gotcha -- if you don't std::move
the arguments into the task (or otherwise induce a move
on that call, like using temporaries), you'll end up with references to them rather than copies of them in the void*
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With