Search code examples
c++c++11templatesvariadic-templates

Adding another function as a parameter after F&&,Args &&... parameters in a template function C++


I'm writing a async member function for my EventLoop class, which would posts the heavy computational task to a global thread pool, and then gets the result of that task and queues a finish callback function that takes the result of the previous work as a parameter back to the EventLoop to let the eventloop handle(call) the finish callback later.

So generally the async member function should takes two functions as the parameter, the first one is designed to be a task function of any return type, the second one is a finish callback that takes the return value of the first function as a parameter.

My first attempt code is as follows:

class EventLoop
{

public:
    using DeferCallback=std::function<void()>;
    //call by another thread to queue work into the eventloop
    void queue_work(DeferCallback cb);


template<typename F,typename ...Args>
void async(F &&task_func,Args&& ...args,std::function<void(std::invoke_result_t<F,Args...>&)> &&finish_cb){
    g_threadpool::get_instance()->post([this,task_func,args...,finish_cb](){
        using task_ret_t=std::invoke_result_t<F,Args...>;
        task_ret_t res=task_func(args...);
        this->queue_work([finish_cb,&res](){
            finish_cb(res);
        });
    });
}

//for task func of void return type
template<typename F,typename ...Args>
void async(F &&task_func,Args&& ...args,std::function<void(void)> &&finish_cb){
    g_threadpool::get_instance()->post([this,task_func,args...,finish_cb](){
        task_func(args...);
        this->queue_work([finish_cb](){
            finish_cb();
        });
    });
}

I tested and found that it only works when I don't pass anything to ...args, or the compiler couldn't work correctly. Then I do some search and find the following question: Parameter pack must be at the end of the parameter list... When and why? It basically tells me:

[...]If a template-parameter of a primary class template or alias >template is a template parameter pack, it shall be the last template->parameter.[...]

And if I have to explicitly instantiate the async function it will be hard to use.

Then I try this:

template<typename Ret,typename ...Args>
void async(std::function<Ret(Args...)> &&task_func,std::function<void(Ret&)> &&finish_cb){ ... }

and find out that I can't pass lambda function or member function except a std::function to the first parameter, which is not ideal. related question:Deduce template argument from std::function call signature

So is there any way to work around?


Solution

  • And if I have to explicitly instantiate the async function it will be hard to use.

    In your case it's particularly hard because, given the signature

    template <typename F, typename ... Args>
    void async (F &&task_func,
                Args && ...args,
                std::function<void(std::invoke_result_t<F,Args...>&)> &&finish_cb)
    

    (and ignoring the problem that Args... args should be in last position) both F and Args... are present also in the type of the last argument (finish_cb), so the deduction of both F and Args... depend also from the last parameter than must be coherent with preceding arguments.

    So, by example, you can't pass a lambda to the last argument because a lambda can be converted to a std::function but isn't a std::function, so F and Args... can't be deduced from the lambda.

    So is there any way to work around?

    Suggestion: avoid at all std::function and place the two callable argument before, the following (variadic) arguments after.

    I mean... something as follows (but caution: code not tested and perfect forwarding ignored)

    template <typename F1, typename F2, typename ...Args>
    void async (F1 && task_func, F2 && finish_cb, Args && ... args) {
        g_threadpool::get_instance()->post([this,task_func,args...,finish_cb](){
            auto res=task_func(args...);
            this->queue_work([finish_cb,&res](){
                finish_cb(res);
            });
        });
    }
    

    If your problem is that you need two version of async(), one where F1 return void (doesn't return a value) and one where F1 return a value, you can use std::invoke_result_t together with SFINAE

    // void (no res) version
    template <typename F1, typename F2, typename ...Args>
    std::enable_if_t<true == std::is_same_v<void, std::invoke_result_t<F,Args...>>>
          async (F1 && task_func, F2 && finish_cb, Args && ... args)
     { /* do something */ }
    
    // non-void (with res) version
    template <typename F1, typename F2, typename ...Args>
    std::enable_if_t<false == std::is_same_v<void, std::invoke_result_t<F,Args...>>>
          async (F1 && task_func, F2 && finish_cb, Args && ... args)
     { /* do something */ }