When you call your function template, e.g., like so
run_callback(printint, 1);
the compiler will try to deduce template arguments which, when substituted into the function parameter types, will make the function parameter types match the types of the arguments. The problem here is that there are no Args...
that you could put into const std::function<void(Args...)>&
to make that type match the type of printint
, which is void(int)
. Therefore, template argument deduction fails.
If you just want your run_callback
to use any function with a signature that matches Args...
, have it take a reference to a function with such signature:
template <typename... Args>
void run_callback(void (&func)(Args...), Args... as)
{
}
live example
This, however, will be somewhat brittle as it basically requires that the types in Args
are exact matches for the parameter types of the callback. Most likely, you'd want run_callback
to work with any function (or, more general, any callable) that could be called with the given args
. One way to achieve this would be to have your function template accept any type as a callback but only enable the overload when the func
parameter is actually a callable that is invocable using the respective argument list:
template <typename F, typename... Args>
auto run_callback(F&& f, Args&&... as) -> std::enable_if_t<std::is_invocable_v<F, Args...>>
{
f(std::forward<Args>(as)...); // call the callback
}
live example
Finally, if, for some reason, you really absolutely need your func
parameter the be an std::function
, you could hide the Args...
parameter pack in func
in a non-deduced context:
template <typename... Args>
void run_callback(const std::common_type_t<std::function<void(Args...)>>& func, Args... as)
{
}
live example
The trick here is the use of std::common_type_t<T>
, which really is just a shorthand for std::common_type<T>::type
, which will just come out to be T
again in the end (Note: there's nothing special about std::common_type
except that it's already there for us to use; we could use any other helper template; all we need is something that forwards its argument to a dependent name). It's impossible in general to unambiguously deduce which T
you'd have to plug into C<T>::xyz
to make C<T>::xyz
become a certain type. It's not even guaranteed that there is such an xyz
for every single T
or that C<T>::xyz
would be a type to begin with. For this reason, the nested-name-specifier in a qualified-id is defined to be a non-deduced context [temp.deduct.type]/5.1. Long story short, that means that Args...
now appears in the type of the parameter func
in such a way that the compiler will not try to deduce what Args...
should be from an argument passed for the func
parameter (i.e., it will not try to do the thing that caused your original code to fail). It still can deduce Args...
from the as
function parameter pack, however. Thus, type deduction will just make Args...
be whatever the types of the arguments passed for as...
are and succeed. After type deduction succeeded, the deduced Args...
are substituted back into the remaining parameters and the type of func
will come out to be const std::function<void(Args...)>&
but now with the types of Args...
taken from what they were deduced to be from the as
parameter pack.
Another way to do basically the same thing would be to, e.g., wrap the argument for the first parameter in an initializer list:
run_callback({ printint }, 1);
live example
An argument that is an initializer list also makes the parameter a non-deduced context in this case [temp.deduct.type]/5.6, so the explanation for why this works is basically the same as in the previous example. Note that, while the previous approach solves the problem from within the template declaration, this approach solves it at the site of the function call.