12

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:

  1. Store the decayed versions of the variadic arguments in an object allocated on the heap (this is a simplification; an Allocator could be used instead), and pass this as the void* parameter,
  2. Pass a generated call-island function as the entry point, with signature void(void*), that calls the target function with the stored parameters, and
  3. (This is the part I can't figure out) have the compiler deduce the types of the argument list from the target function's signature, to follow the Don't Repeat Yourself principle.
  4. Note that the function pointer and its argument types are known and resolved at compile time, and the actual argument values passed to the function are not known until runtime (because they include things like object pointers and runtime configuration options).

In 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;
}
Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
Mike DeSimone
  • 41,631
  • 10
  • 72
  • 96
  • 3
    The only thing you can do is `Task` with your current setup. My question is, though - why have the function pointer as a template parameter? If you get rid of that requirement, you can just do `auto tfoo = make_task(foo);` with full deduction. – Xeo May 26 '13 at 15:48
  • You may want to look [here](http://stackoverflow.com/a/9045644/775806). – n. m. could be an AI May 26 '13 at 16:29
  • Why use templates when typedef and a param will do? – Warren P May 27 '13 at 01:30
  • @Xeo: I want the function pointer as a template parameter so it gets resolved at compile time, and thus hopefully the optimizer gets a shot at inlining it. if the function pointer still exists at runtime, then it has to be passed through to the task via the `void*` parameter. – Mike DeSimone May 27 '13 at 01:39
  • @Warren: I have about six different tasks and I don't want to have to write a separate boilerplate glue function for each. Ideally, this can all get packed into the `Task` constructor at the user API level. – Mike DeSimone May 27 '13 at 01:41
  • 1
    I think I figured out what I missed. You want a C++14/17 feature, namely being able to have inferred type parameters to a `template`. `Task`, which you can do via a macro if you don't like typeing `foo` twice, is the way you have to do it in C++11. In C++14 or 17, they are going to have the ability for type parameters to a `template` be deduced from later non-type parameter types. – Yakk - Adam Nevraumont May 27 '13 at 01:44
  • @n.m.: THat's a case where someone was trying to pass something that was available at runtime as an implicitly compile-time template parameter. Here I'm trying to resolve everything at compile time, and thus want the function pointer specified at compile time, not passed in at runtime. The function's *arguments* are passed in at runtime, and hence are decayed, stored, and later fetched by the `TaskArgs` class. – Mike DeSimone May 27 '13 at 01:45
  • Well, if it's a C++>11 feature, then I'll just have to give up on arbitrary argument lists. I can still do a working single-argument version (because then I can define `template`), which will have to do and people will just need to pass a `std::tuple` or something. – Mike DeSimone May 27 '13 at 02:04
  • @MikeDeSimone The C++14/17 feature was only needed to remove repeating the name of the function twice. Getting rid of the `Args` is relatively easy in C++11. :) – Yakk - Adam Nevraumont May 27 '13 at 02:09
  • 1
    Oh, and I should mention the less extreme solution: `template class Task; template class Task { /* old body */ };` which lets you deduce `Args` from the type of the function. That is what the `decltype` comments are about. You create a non-existent base implementation, then specialize to pattern match the `Args...` – Yakk - Adam Nevraumont May 27 '13 at 13:44
  • For the record, I was able to compile Yakk's code in a generic environment, so it seems like it might work, but I'm unable to fit that solution into the form I gave above, so I couldn't use it. I wound up shipping a version which dropped support for variable argument lists, and instead required exactly one argument (of arbitrary type, of course), with a specialization for `void*` (which is what the C code uses). There may yet be a better answer out there, but I haven't had time to find it. – Mike DeSimone Oct 14 '13 at 02:53

1 Answers1

4

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*.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Actually, I had this working with lambdas before. It's the code-bloated case I'm trying to improve on, because using lambdas leads to two things: 1) something effectively the same as two compiler-generated classes with a virtual function: one abstract with just `virtual void lambda(void) = 0` and the second final class with the implementation, and 2) the need for a "throw bad_func" function (even with `-fno-exceptions`), similar to how you have to define `__cxa_pure_virtual` to handle a pure virtual call. I managed to `#pragma weak` #2 to something I had, but not the vtbl bloat from #1. – Mike DeSimone May 27 '13 at 01:57
  • BTW, the lambda version was really easy: the `Task` constructor just took a `std::function` that it passed through the param. Then the caller just passed something like `[&](){myTask(x, y, z);}`. – Mike DeSimone May 27 '13 at 02:00
  • 1
    @MikeDeSimone I rewrote it, what is above is version 3. `std::function` was eliminated in version 2, because as you noted that is a `virtual` function overhead. Lambda does not mean `std::function` -- `std::function` is type erasure on top of Lambdas. The above should bind the function pointer at compile-time, and pass the arguments in at run-time. I should probably return a `std::tuple<...>*` so deleting it is safe, let me fix that. – Yakk - Adam Nevraumont May 27 '13 at 02:04
  • @MikeDeSimone Version 3 added compile-time resolution of the function "pointer" you pass in. While `MAKE_TEST(test)` looks like a function call, you'll note it resolves to passing `test` to a `template` only. – Yakk - Adam Nevraumont May 27 '13 at 02:07
  • Actually, FreeRTOS doesn't really have an organized means of deleting a task. When you delete a task (if the option to delete a task is even enabled), it just blows away its context and stack right there. No chance at cleanup. So proper deletion support is pretty much academic. – Mike DeSimone May 27 '13 at 02:08
  • @MikeDeSimone Ok, it now returns a fully typed "`void*`" if you really want to delete it. I could also return it on the stack if you prefer. It also passes the function "pointer" via `template` parameters all the way down, so even a brain-dead optimizer should be able to figure that out. – Yakk - Adam Nevraumont May 27 '13 at 02:17
  • It took me a while to deduce how your code works, but it looks like your code is treating the argument list as a variadic list of integers set at compile time. If so, this is not the case, and I have added a #4 to the question to try to clarify this. – Mike DeSimone May 27 '13 at 02:19
  • @MikeDeSimone Nope, the types of the arguments are deduced at compile time (because types must be), but the values are determined at run time. There are two optimizations missing: first, allowing the `void*` to be stored outside the heap, and second, I reconvert the `Args` on each `void(*)(void*)` call. That second one will only be "expensive" if you use "small" arguments like `char`, and pass them to `MAKE_TASK` as `int`, or similar. The compile time list of integers -- `seq<>` -- is actually how I pass instructions on how to unpack the `tuple` into the function. `std::string` version added. – Yakk - Adam Nevraumont May 27 '13 at 02:25