如何编写具有可变参数的成员函数作为模板参数

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

是否可以像下面这样用 C++14 编写模板函数

这是示例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)...);
}

所以,为了测试 class

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

  void FuncInt(int){};
};

模板可以用于下面的用例(但失败了)

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;
}

这里是错误信息。

#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

我想通了。静静的喜欢std::men_fn.

这是下面的代码和示例https://godbolt.org/z/NoWPV_


#include <type_traits>

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

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

  void FuncInt(int){};
};

int main()
{
  Test test;
  Proxy<Test> proxy;

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

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

  return 0;
}

问题

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

就是Args推导了两次,应该是一样的

有几种方法可以解决该问题:

  • 添加额外的模板参数:

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

    Demo

  • 或使参数不可推导(我使用 C++20 中的 std::type_identity,但可以在以前的版本中简单地重新实现):

    template <typename T, typename R, typename... Args>
    decltype(auto) Call(T& obj, R(T::*mf)(Args...), std::type_identity_t<Args>... args)
    {
        return (obj.*mf)(std::forward<Args>(args)...);
    }
    
  • 或完全更改签名:

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

    Demo

在您的 Call 声明中:

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

函数模板采用(或可能尝试推断)两个或更多模板参数:第一个是 T,第二个是 R,其余是 Args。因此,在 Call<int()>Call<bool(bool)> 中将单个函数类型作为第一个模板参数是错误的。正确的调用方式是

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

另一个问题是,如果你想推导模板参数,就像在非重载的情况下一样,因为 Args 包出现了两次,它只有在列表从成员函数和从尾随参数完全相同:

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.

如果你更喜欢函数类型语法,你可以使用:

的解决方案
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 是声明指向成员的指针的语法,通常用于指向数据成员,但如果 FuncT 类型为一个函数类型。 (我添加了 constexpr 和条件异常说明符以使其更通用。)

这也解决了原始问题,它不能用于调用 const 或具有 ref 限定符的成员函数,因为这会创建不同的函数类型:

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);
}

但是对于新的 Call 定义,driver_Test2 很好,因为任何非静态成员函数都可以匹配 FuncT T::*。如果我们想为 driver_Test2 中的调用提供一个模板参数,可能是因为成员函数被重载了,那看起来像 Call<int() const>Call<void() &>.