Search code examples
c++c++11templatesc++14variadic-templates

How to write a member function with variadic parameters as template parameter


Is it possible to write template function in C++14 like below

Here is the sample https://godbolt.org/z/9gRk-t

// pseudo code

#include <type_traits>

template <typename T, typename R, typename... Args>
decltype(auto) Call(T& obj, R(T::*mf)(Args...), Args&&... args) 
{
  return (obj.*mf)(std::forward<Args>(args)...);
}

So, for a test class

struct Test 
{
  int Func(){return 1;};
  bool Func(bool){return true;};  // overload

  void FuncInt(int){};
};

The template coudl work for the use case below (but it failed)

int main()
{
  Test test;

  // for overload case
  auto a = Call<int()>(test, &Test::Func);
  auto b = Call<bool(bool)>(test, &Test::Func, true);

  // for non-overload case
  Call(test, &Test::FuncInt, 1);

  return 0;
}

Herer the erro info.

#1 with x64 msvc v19.24
example.cpp
<source>(23): error C2672: 'Call': no matching overloaded function found
<source>(23): error C2770: invalid explicit template argument(s) for 'decltype(auto) Call(T &,R (__cdecl T::* )(Args...),Args &&...)'
<source>(5): note: see declaration of 'Call'
<source>(24): error C2672: 'Call': no matching overloaded function found
<source>(24): error C2770: invalid explicit template argument(s) for 'decltype(auto) Call(T &,R (__cdecl T::* )(Args...),Args &&...)'
<source>(5): note: see declaration of 'Call'
Compiler returned: 2

Solution

  • In your declaration of Call:

    template <typename T, typename R, typename... Args>
    decltype(auto) Call(T& obj, R(T::*mf)(Args...), Args&&... args);
    

    the function template takes (or might try to deduce) two or more template arguments: the first is T, the second is R, and the rest are Args. So giving a single function type as the first template argument as in Call<int()> and Call<bool(bool)> is wrong. The correct way to call it would be

    auto a = Call<Test, int>(test, &Test::Func);
    auto b = Call<Test, bool, bool>(test, &Test::Func, true);
    

    Another issue is that if you want the template arguments deduced, as in the non-overloaded case, since the Args pack appears twice, it will only work if the lists deduced from the member function and from the trailing arguments are exactly the same:

    int n = 3;
    Call(test, &Test::FuncInt, n); // error!
    // Args... is deduced to `int` from `&Test::FuncInt`, but deduced to `int&`
    // from `n` since it's an lvalue matching a forwarding reference parameter.
    

    If you prefer the function type syntax, you could use the solution of @foo:

    template <typename FuncT, typename T, typename... Args>
    constexpr decltype(auto) Call(T& obj, FuncT T::*mf, Args&&... args)
        noexcept(noexcept((obj.*mf)(std::forward<Args>(args)...)))
    {
        return (obj.*mf)(std::forward<Args>(args)...);
    }
    
    // main() exactly as in question, including Call<int()> and Call<bool(bool)>.
    

    FuncT T::*mf is the syntax for declaring a pointer-to-member, which is often used to point to a data member, but also works to point to a function if the type FuncT is a function type. (I've added the constexpr and conditional exception specifier to make it more generic.)

    This also solves an issue with the original, which can't be used to invoke a member function which is const or which has a ref-qualifier, since this creates a different function type:

    class Test2 {
    public:
        int get() const;
        void set() &;
    };
    
    void driver_Test2() {
        Test2 test;
    
        // Error with original Call:
        // Type of &Test2::get is "int (Test2::*)() const",
        // which cannot match "R (Test2::*)(Args...)"
        int a = Call(test, &Test2::get);
    
        // Error with original Call:
        // Type of &Test2::set is "void (Test2::*)(int) &",
        // which cannot match "R (Test2::*)(Args...)"
        Call(test, &Test2::set, 1);
    }
    

    But with the new Call definition, driver_Test2 is fine, since any non-static member function can match FuncT T::*. If we wanted to supply a template argument to the calls in driver_Test2, maybe because the member functions are overloaded, that would look like Call<int() const> and Call<void() &>.