Search code examples
c++templatesfunction-pointersvariadic-templatesvariadic

DescribeFunction Parameters With Variadic Template Types


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:

  1. Given a function of the signature void bar(bool, char*, int*), I'd like to call: foo(bar, false)
  2. Given a function of the signature void bar(bool, int*, int*), I'd like to call: foo(bar, false)
  3. Given a function of the signature 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 for void (__cdecl* )(ARGS...,T *,int* ) from void (bool,char *,int* )

What can I do to help with the deduction?


Solution

  • 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:

    1. We now have Args... and UArgs.... This is because we want to capture N arguments types for func, but we only want N-2 arguments for params
    2. We now match void(*func)(Args...) instead of void(*func)(Args...,T*,int*). T* is no longer a template parameter
    3. We have this long std::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.