Search code examples
c++templateslambdametaprogrammingfunctor

How to write typed wrapper for a thread pool?


I have a simple thread pool. It takes tasks and distributes them among threads using round-robin. The task looks like this

using TaskFn = void (*)(void*);

struct Task {
    TaskFn fn;
    void* args;
};

Just two pointers: to a function that takes void* and to the argument itself. The thread pool calls Task::fn and passes Task::args. Everything works well.

But I wanted to write a typed wrapper for this whole thing. So I could write like this:

Task some_task = MakeTask([](int a, int b){
        // Task body
}, 123, 456);

I don’t need the closures to work. I wrote code that does not compile:

template <typename Function, typename ... Args>
void DoCall(Function f, std::tuple<Args...>* args) {
    auto callable = [args, f](){
        std::apply(f, *args);
    };
    callable();
}

template <typename Function, typename ... Args>
Task MakeTask(Function f, Args ... args) {
    Task task;
             
    std::tuple<Args...>* args_on_heap = new std::tuple<Args...>(args...);
    task.args = (void*) args_on_heap;

    TaskFn fn = [](void* ptr){
        // The problem here is that I can’t pass `f` here without creating a closure.
        // But if I create a closure, the signature will be different. 
        // In theory, everything that is needed is known at the compilation stage. 
        // But how to curb the compiler?
        DoCall<Function, Args...>(f, (std::tuple<Args...>*) ptr);
    };

    task.fn = fn;
    return task;
    
    // P.S I know that args_on_heap leaks.
}

So, questions:

  1. Is it possible to implement what I have in mind?
  2. If yes, how to do it? In what direction should I dig? What features of the language (which I probably don’t know about yet) will help me implement what I have in mind.
  3. If I can’t implement this, then what are the alternatives?

Thank you in advance :)


Solution

  • As written, you’re accepting even callable objects with state, so the language will force you to account for that state. You could copy it into a control block along with your arguments, but if support for state isn’t the point you can require the function to be a template argument:

    template <auto &F, typename ... Args>
    Task MakeTask(Args ... args) {
        using T = std::tuple<Args...>;
        return {[](void* ptr){
            std::unique_ptr<T> p (static_cast<T*>(ptr));
            DoCall<F, Args...>(f, p.get());
        }, new T(args...)};
    }
    

    Note that passing a lambda directly as a template argument requires C++20. You can work around that with a constexpr variable:

    constexpr auto *f = [](…) {…};
    MakeTask<*f>(…);