有没有办法部分匹配可变参数模板参数包?

Is there a way to partially match a variadic template parameter pack?

我目前有一个系统可以 "connect" signal 运行。这个 signal 是一个可变参数模板,它的模板参数是它可以 connect 到的函数的参数。

在当前的实现中,我显然无法连接到参数与 signal 的参数不完全相同(或可以转换为)的函数。现在,当我试图模仿 Qt 的 signal/slot/connect 时,我还想将 signalN 参数连接到M<N 参数中的 slot,这是完全明确定义的(即忽略信号的 >M 参数,只将第一个 M 传递给连接的函数)。有关我最简单形式的代码示例,请参阅 Coliru.

所以问题有两个方面:

  1. 如何使 connect 函数调用工作 void g(int);
  2. 如何使 emit 调用对函数 void g(int); 有效?

我猜我必须为 slot 及其调用函数制作一些 "magic" 参数包缩减器,但我看不出它们应该如何组合在一起所以真正开始尝试编写解决方案非常困难。如果至少 Clang/GCC 和 Visual Studio 2015 可以编译它,我可以使用仅限 C++17 的解决方案。

为了完整起见,上面链接的代码:

#include <memory>
#include <vector>

template<typename... ArgTypes>
struct slot
{
    virtual ~slot() = default;

    virtual void call(ArgTypes...) const = 0;
};

template<typename Callable, typename... ArgTypes>
struct callable_slot : slot<ArgTypes...>
{
    callable_slot(Callable callable) : callable(callable) {}

    void call(ArgTypes... args) const override { callable(args...); }

    Callable callable;
};

template<typename... ArgTypes>
struct signal
{
    template<typename Callable>
    void connect(Callable callable)
    {
        slots.emplace_back(std::make_unique<callable_slot<Callable, ArgTypes...>>(callable));
    }

    void emit(ArgTypes... args)
    {
        for(const auto& slot : slots)
        {
            slot->call(args...);
        }
    }

    std::vector<std::unique_ptr<slot<ArgTypes...>>> slots;
};

void f(int, char) {}

int main()
{
    signal<int, char> s;
    s.connect(&f);

    s.emit(42, 'c');
}
template<class...> struct voider { using type = void; };
template<class... Ts> using voidify = typename voider<Ts...>::type;

template<class C, class...Args>
using const_lvalue_call_t = decltype(std::declval<const C&>()(std::declval<Args>()...));

template<class T, std::size_t...Is>
auto pick_from_tuple_impl(T &&, std::index_sequence<Is...>) 
    -> std::tuple<std::tuple_element_t<Is, T>...>;

template<class Tuple, class = std::enable_if_t<(std::tuple_size<Tuple>::value > 0)>>
using drop_last = decltype(pick_from_tuple_impl(std::declval<Tuple>(), 
                        std::make_index_sequence<std::tuple_size<Tuple>::value - 1>()));

template<class C, class ArgsTuple, class = void>
struct try_call
    : try_call<C, drop_last<ArgsTuple>> {};

template<class C, class...Args>
struct try_call<C, std::tuple<Args...>, voidify<const_lvalue_call_t<C, Args...>>> {
    template<class... Ts>
    static void call(const C& c, Args&&... args, Ts&&... /* ignored */) {
        c(std::forward<Args>(args)...); 
    }
};

然后在callable_slot:

void call(ArgTypes... args) const override {
     using caller = try_call<Callable, std::tuple<ArgTypes...>>;
     caller::call(callable, std::forward<ArgTypes>(args)...); 
}

对于成员指针支持(这需要 SFINAE 友好 std::result_of),将 const_lvalue_call_t 更改为

template<class C, class...Args>
using const_lvalue_call_t = std::result_of_t<const C&(Args&&...)>;

然后将try_call::call中的实际调用改为

std::ref(c)(std::forward<Args>(args)...); 

这是穷人的 std::invoke 左值可调用项。如果你有 C++17,直接使用 std::invoke(并使用 std::void_t 而不是 voidify,虽然我喜欢后者的声音)。

不确定您到底想要什么但是... std::tuplestd::make_index_sequence ...

首先你需要一个类型特征,它给你一个函数的参数数量(或std::function

template <typename>
struct numArgs;

template <typename R, typename ... Args>
struct numArgs<R(*)(Args...)> 
      : std::integral_constant<std::size_t, sizeof...(Args)>
 { };

template <typename R, typename ... Args>
struct numArgs<std::function<R(Args...)>>
      : std::integral_constant<std::size_t, sizeof...(Args)>
 { };

接下来你必须在callable_slot中添加一个constexpr值来记住Callable函数

中的参数个数
static constexpr std::size_t numA { numArgs<Callable>::value };

然后你必须修改 call() 方法以将参数打包在 std::tuple<ArgTypes...> 中并调用另一个方法传递元组和从 0 到 numA[=29 的索引序列=]

void call(ArgTypes... args) const override
 { callI(std::make_tuple(args...), std::make_index_sequence<numA>{}); }

最后你必须在 CallI() 中调用 callable() 函数,该函数仅包含参数元组的前 numA 个元素

template <std::size_t ... Is>
void callI (std::tuple<ArgTypes...> const & t,
            std::index_sequence<Is...> const &) const
 { callable(std::get<Is>(t)...); }

以下是完整的工作示例

#include <memory>
#include <vector>
#include <iostream>
#include <functional>

template <typename>
struct numArgs;

template <typename R, typename ... Args>
struct numArgs<R(*)(Args...)> 
   : std::integral_constant<std::size_t, sizeof...(Args)>
 { };

template <typename R, typename ... Args>
struct numArgs<std::function<R(Args...)>>
   : std::integral_constant<std::size_t, sizeof...(Args)>
 { };

template <typename ... ArgTypes>
struct slot
 {
   virtual ~slot() = default;

   virtual void call(ArgTypes...) const = 0;
 };

template <typename Callable, typename ... ArgTypes>
struct callable_slot : slot<ArgTypes...>
 {
   static constexpr std::size_t numA { numArgs<Callable>::value };

   callable_slot(Callable callable) : callable(callable)
    { }

   template <std::size_t ... Is>
   void callI (std::tuple<ArgTypes...> const & t,
               std::index_sequence<Is...> const &) const
    { callable(std::get<Is>(t)...); }

   void call(ArgTypes... args) const override
    { callI(std::make_tuple(args...), std::make_index_sequence<numA>{}); }

   Callable callable;
 };

template <typename ... ArgTypes>
struct signal
 {
   template <typename Callable>
   void connect(Callable callable)
    {
      slots.emplace_back(
         std::make_unique<callable_slot<Callable, ArgTypes...>>(callable));
    }

   void emit(ArgTypes... args)
    { for(const auto& slot : slots) slot->call(args...); }

   std::vector<std::unique_ptr<slot<ArgTypes...>>> slots;
 };

void f (int i, char c)
 { std::cout << "--- f(" << i << ", " << c << ")" << std::endl; }

void g (int i)
 { std::cout << "--- g(" << i << ")" << std::endl; }

struct foo
 {
   static void j (int i, char c)
    { std::cout << "--- j(" << i << ", " << c << ")" << std::endl; }

   void k (int i)
    { std::cout << "--- k(" << i << ")" << std::endl; }
 };

int main ()
 {
   std::function<void(int, char)> h { [](int i, char c)
       { std::cout << "--- h(" << i << ", " << c << ")" << std::endl; }
    };

   std::function<void(int)> i { [](int i)
       { std::cout << "--- i(" << i << ")" << std::endl; }
    };

   using std::placeholders::_1;

   foo foo_obj{};

   std::function<void(int)> k { std::bind(&foo::k, foo_obj, _1) };

   signal<int, char> s;

   s.connect(f);
   s.connect(g);
   s.connect(h);
   s.connect(i);
   s.connect(foo::j);
   s.connect(k);

   s.emit(42, 'c');
 }

此示例需要 C++14,因为使用 std::make_index_sequencestd::index_sequence

替换它们并准备一个符合 C++11 的解决方案并不难。