I want to write a template function which takes in a function pointer and all but the last 2 arguments to that function. Something like this:
template <typename T, typename... ARGS>
void foo(void(*func)(ARGS..., T*, int*), ARGS... params);
I'd like to do something like the following:
void bar(bool, char*, int*)
, I'd like to call: foo(bar, false)
void bar(bool, int*, int*)
, I'd like to call: foo(bar, false)
void bar(char*, int*)
, I'd like to call: foo(bar)
But when I try to define foo
like this I get the error:
error C2672:
foo
: no matching overloaded function found
error C2784:void foo(void (__cdecl *)(ARGS...,T* ,int *),ARGS...)
: could not deduce template argument forvoid (__cdecl* )(ARGS...,T *,int* )
fromvoid (bool,char *,int* )
What can I do to help with the deduction?
The reason this is not working is because ARGS...
is a variadic pack, and when used for deduction it can only be used at the end of a function signature. For example, you could deduce:
void(*)(int,Args...)
But you cannot deduce
void(*)(Args...,int)
Since your problem requires the last argument to be a specific kind, and the second-last to be deduced specifically, you will need to deduce the entire function signature of func
, and use SFINAE to prevent accidentally invoking this function with the wrong arguments.
To do that, we first need a way to extract out the nth
last parameter. A simple type trait for this could be written as follows:
#include <type_traits>
// A simple type-trait that gets the Nth type from a variadic pack
// If N is negative, it extracts from the opposite end of the pack
// (e.g. -1 == the last entry)
template<int N, bool IsPositive, typename...Args>
struct extract_nth_impl;
template<typename Arg0, typename...Args>
struct extract_nth_impl<0,true,Arg0,Args...> {
using type = Arg0;
};
template<int N, typename Arg0, typename...Args>
struct extract_nth_impl<N,true,Arg0,Args...>
: extract_nth_impl<N-1,true,Args...>{};
template<int N, typename...Args>
struct extract_nth_impl<N,false,Args...> {
using type = typename extract_nth_impl<(sizeof...(Args)+N),true,Args...>::type;
};
// A type-trait wrapper to avoid the need for 'typename'
template<int N, typename...Args>
using extract_nth_t = typename extract_nth_impl<N,(N>=0),Args...>::type;
We can use this to extract the last parameter to ensure it's int*
, and the second-last parameter to know it's type (T*
). Then we can just use std::enable_if
to SFINAE-away any bad-inputs, so that this function will fail to compile if misused.
template<
typename...Args,
typename...UArgs,
typename=std::enable_if_t<
(sizeof...(Args) >= 2) &&
(sizeof...(Args)-2)==(sizeof...(UArgs)) &&
std::is_same_v<extract_nth_t<-1,Args...>,int*> &&
std::is_pointer_v<extract_nth_t<-2,Args...>>
>
>
void foo(void(*func)(Args...), UArgs&&...params)
{
// your code here, e.g.:
// bar(func, std::forward<UArgs>(params)...);
}
Note: The template and the signature has changed in the following ways:
Args...
and UArgs...
. This is because we want to capture N arguments types for func
, but we only want N-2
arguments for params
void(*func)(Args...)
instead of void(*func)(Args...,T*,int*)
. T*
is no longer a template
parameterstd::enable_if_t
which is used to SFINAE away bad cases, such as N<2
, too many params for the number of signature args, T*
(second-last argument) not being a pointer, and the last signature arg being int*
But overall this works. If T
was needed in the definition of the function, you can extract it easily with:
using T = std::remove_point_t<extract_nth_t<-2,Args...>>;
(Note: remove_pointer_t
used to match only against the type, and not the pointer)
The following test cases work for me using clang-8.0
and -std=c++17
:
void example1(bool, char*, int*){}
void example2(bool, int*, int*){}
void example3(char*, int*){}
void example4(char*, char*){}
int main() {
foo(&::example1,false);
// foo(&::example1); -- fails to compile - too few arguments (correct)
foo(&::example2,false);
// foo(&::example2,false,5); -- fails to compile - too many arguments (correct)
foo(&::example3);
// foo(&::example4); -- fails to compile - last argument is not int* (correct)
}
Edit: As pointed out by @max66, this solution does not constrain convertible types as input to param
. This does mean that it may fail if any param
is not properly convertible. A separate condition to std::enable_if
could be added to rectify this, if this is an important quality attribute of the API.