推导多个参数包

8

背景

我正在尝试为一个仅限于模板的单元测试库编写一些模板函数,具体来说是针对Qt。

问题

在这个库中,我有一个可变参数模板,它接收变量数量的对象和函数(实际上是Qt5信号),总是成对出现,如QObject、signal等...然后最好跟随变量数量的信号参数。

期望解决方案

// implementation.h

template <typename T, typename U, typename... Sargs, typename... Fargs>
void test_signal_daisy_chain(T* t,  void(T::*t_signal)(Fargs...), 
                             U* u,  void(U::*u_signal)(Fargs...), 
                             Sargs... sargs, 
                             Fargs... fargs) {...}

// client.cpp

test_signal_daisy_chain(object, &Object::signal1, 
                        object, &Object::signal2, 
                        object, &Object::signal3, 
                        1, 2, 3); // where the signals are defined as void(Object::*)(int, int, int)

Fargs...对应于t_signalu_signal中的参数以及要传递给此函数进行测试的参数,而Sargs...对应于变量数量的QObject和信号成员函数(void(T::*)(Fargs...)),其目的是为了进行测试而发出信号。

毫不奇怪,由于“模板参数推断/替换失败”,我得到了“没有匹配的函数”的错误提示,我的ClangCodeModel插件警告说需要6个参数,而实际给出了8个。

工作(丑陋)的解决方案

// implementation.h
template <typename... Fargs>
struct wrapper
{
    template <typename T, typename U, typename... Sargs>
    void test_signal_daisy_chain(Fargs... fargs, 
                                 T* t,  void(T::*t_signal)(Fargs...), 
                                 U* u,  void(U::*u_signal)(Fargs...), 
                                 Sargs... sargs) {...}

// client.cpp

wrapper<int, int, int>::test_signal_daisy_chain(1, 2, 3, 
                                                object, &Object::signal1,
                                                object, &Object::signal2,
                                                object, &Object::signal3);

我不满意在函数调用的开头和包装器模板类型参数中都必须明确定义变量函数参数。事实上,我最初感到惊讶的是,它们不能仅仅通过与函数对象的可变参数匹配来推导出来。我愿意使用包装函数而不是包装类,因为我已经设置了一个细节命名空间,为了提供一个干净和用户友好的API,我愿意使其变得杂乱无章。

注意:信号参数可以是从基本类型到用户定义类型、POD结构体到模板类的任何长度的变量。

编辑1: c++11是硬性要求,所以您可以在答案中保留>c++11功能,只要它们有一些c++11解决方法,例如auto...很容易修复,auto myFunction = []() constexpr {...};则不然。如果使用if constexpr代替递归template 帮助函数可以节省空间,并提供更简洁、完整和具有未来性的答案,请选择您认为最好的标准。


在您期望的解决方案中,您希望如何将 SargsFargs 分隔开? - bipll
Sargs 应该由 T(回顾起来应该继承 QObject)分割,并且另一个 void(T::*)(Fargs)。我需要递归地遍历它们,以便最终再次使用 test_signal_daisy_chain(u, u_signal, sargs..., fargs...) 调用它。事后看来,我可能需要在我的参数中声明 F f, Fargs... fargs,以便我的扩展正常工作。 - jfh
1
我认为唯一可行的方式是使用签名 void test(T *t, void (T::*t_signal)(Fargs...), U*u, void (U::*u_signal)(Fargs...), Args... args),然后将 Args 打包到类型元组中,并在此基础上提取 Sargs 类型(通过从末尾截断 Fargs)和其他对象-信号对(如果有的话)。当然,如果您计划进行任何重载,则这种方法将不起作用。 - Kuba hasn't forgotten Monica
@KubaOber 我同意,我认为唯一能给我一个漂亮干净的前端API的解决方案需要使用元组进行一些工作。我可能会开始着手实现自己的解决方案,但如果你有一个答案:
  1. 具有类似于 method(Object, ptr, Object, ptr, .../* variable amount of "Object, ptr", Fargs...) 的风格,并且
  2. 不需要显式类型声明或包装器参数, 那么我很乐意看看它。
- jfh
2个回答

4
template<class T>
struct tag_t { using type=T; };
template<class Tag>
using type_t = typename Tag::type;

template<class T>
using no_deduction = type_t<tag_t<T>>;

template <typename T, typename U, typename... Sargs, typename... Fargs>
void test_signal_daisy_chain(
  T* t, void(T::*t_signal)(Sargs...),
  U* u, void(U::*u_signal)(Fargs...),
  no_deduction<Sargs>... sargs,
  no_deduction<Fargs>... fargs)

我假设在`t_signal`中的 `Fargs...` 是一个拼写错误,实际应该是`Sargs`。
如果不是这样,你就会有麻烦了。没有规则说"早期的推断比后来的推断更好"。
中,你可以编写一个返回函数对象的函数:
template <typename T, typename U, typename... Fargs>
auto test_signal_daisy_chain(
  T* t, void(T::*t_signal)(Fargs...),
  U* u, void(U::*u_signal)(Fargs...),
  no_deduction<Fargs>... fargs
) {
  return [=](auto...sargs) {
    // ...
  };
}

然后使用看起来像:
A a; B b;
test_signal_daisy_chain( &a, &A::foo, &b, &B::bar, 1 )('a', 'b', 'c');

中,可以通过手动编写函数对象来实现这一点。

抱歉,Fargs..在t_signal中应该匹配u_signal的参数,而Sargs对应于QObject和成员函数对的可变数量。我会更新以提高清晰度。 - jfh
@jfhcs 我想我已经处理了那种情况。我不得不交换 fargssargs 的顺序。 - Yakk - Adam Nevraumont
我似乎在我的测试用例中运气不太好。此外,对于这个问题,我有一个硬性要求,就是它遵循Qt的约定,即QObject,functor,QObject,functor,就像QObject::connect函数一样。谢谢你的耐心和帮助,我非常感激。 - jfh
@jfhcs 你说得对,那个方法行不通。已添加替代方案。 - Yakk - Adam Nevraumont

4

最简单的方法是在开始时将参数打包成元组,然后将元组传递给test_signal_daisy_chain_impl

template < typename... Fargs, 
          typename T, typename... Sargs>
void test_signal_daisy_chain_impl(const std::tuple<Fargs...> & fargs, 
                                  T* t, void(T::*t_signal)(Fargs...),
                                  Sargs &&... sargs)
{
    // apply unpacks the tuple
    std::apply([&](auto ...params) 
               {
                   (t->*t_signal)(params...);
               }, fargs);

    // Although packed into the tuple, the elements in
    // the tuple were not removed from the parameter list,
    // so we have to ignore a tail of the size of Fargs.
    if constexpr (sizeof...(Sargs) > sizeof...(Fargs))
       test_signal_daisy_chain_impl(fargs, std::forward<Sargs>(sargs)...);
}

// Get a tuple out of the last I parameters
template <std::size_t I, typename Ret, typename T, typename... Qargs>
Ret get_last_n(T && t, Qargs && ...qargs)
{
    static_assert(I <= sizeof...(Qargs) + 1, 
                  "Not enough parameters to pass to the signal function");
    if constexpr(sizeof...(Qargs)+1  == I)
       return {std::forward<T>(t), std::forward<Qargs>(qargs)...};
    else
       return get_last_n<I, Ret>(std::forward<Qargs>(qargs)...);
}    

template <typename T, typename... Fargs, 
          typename... Qargs>
void test_signal_daisy_chain(T* t, void(T::*t_signal)(Fargs...),
                             Qargs&&... qargs)
{
    static_assert((sizeof...(Qargs) - sizeof...(Fargs)) % 2 == 0,
                  "Expecting even number of parameters for object-signal pairs");
    if constexpr ((sizeof...(Qargs) - sizeof...(Fargs)) % 2 == 0) {
        auto fargs = get_last_n<sizeof...(Fargs), std::tuple<Fargs...>>(
                                 std::forward<Qargs>(qargs)...);
        test_signal_daisy_chain_impl(fargs, t, t_signal, 
                                     std::forward<Qargs>(qargs)...);
    }
}

用法:

class Object {
public:
    void print_vec(const std::vector<int> & vec)
    {
        for (auto elem: vec) std::cout << elem << ", ";
    }
    void signal1(const std::vector<int> & vec) 
    { 
        std::cout << "signal1(";
        print_vec(vec);
        std::cout << ")\n";
    }
    void signal2(const std::vector<int> & vec) 
    { 
        std::cout << "signal2(";
        print_vec(vec);
        std::cout << ")\n";
    }
    void signal_int1(int a, int b) 
    { std::cout << "signal_int1(" << a << ", " << b << ")\n"; }
    void signal_int2(int a, int b) 
    { std::cout << "signal_int2(" << a << ", " << b << ")\n"; }
    void signal_int3(int a, int b) 
    { std::cout << "signal_int3(" << a << ", " << b << ")\n"; }
};

int main()
{
   Object object;
   test_signal_daisy_chain(&object, &Object::signal1,
                           &object, &Object::signal2 ,
                           std::vector{1,2,3});
   test_signal_daisy_chain(&object, &Object::signal_int1,
                           &object, &Object::signal_int2 ,
                           &object, &Object::signal_int3,
                           1,2);
}

编辑1

由于C++11是一个硬约束条件,因此有一个更加丑陋的解决方案,基于相同的原则。必须实现诸如std :: applystd :: make_index_sequence之类的内容。使用重载代替if constexpr(....)

template <std::size_t ...I>
struct indexes
{
    using type = indexes;
};

template<std::size_t N, std::size_t ...I>
struct make_indexes
{
    using type_aux = typename std::conditional<
                    (N == sizeof...(I)),
                    indexes<I...>,
                    make_indexes<N, I..., sizeof...(I)>>::type;
    using type = typename type_aux::type;
};

template <typename Tuple, typename T, typename Method, std::size_t... I>
void apply_method_impl(
    Method t_signal, T* t, const Tuple& tup, indexes<I...>)
{
    return (t->*t_signal)(std::get<I>(tup)...);
}

template <typename Tuple, typename T, typename Method>
void apply_method(const Tuple & tup, T* t, Method t_signal)
{
      apply_method_impl(
        t_signal, t, tup,
        typename make_indexes<
             std::tuple_size<Tuple>::value>::type{});
}

template < typename... Fargs,  typename... Sargs>
typename std::enable_if<(sizeof...(Fargs) == sizeof...(Sargs)), void>::type 
test_signal_daisy_chain_impl(const std::tuple<Fargs...> & , 
                             Sargs &&...)
{}

template < typename... Fargs, 
          typename T, typename... Sargs>
void test_signal_daisy_chain_impl(const std::tuple<Fargs...> & fargs, 
                                  T* t, void(T::*t_signal)(Fargs...),
                                  Sargs &&... sargs)
{
    apply_method(fargs, t, t_signal);

    // Although packed into the tuple, the elements in
    // the tuple were not removed from the parameter list,
    // so we have to ignore a tail of the size of Fargs.
    test_signal_daisy_chain_impl(fargs, std::forward<Sargs>(sargs)...);
}

// Get a tuple out of the last I parameters
template <std::size_t I, typename Ret, typename T, typename... Qargs>
typename std::enable_if<sizeof...(Qargs)+1  == I, Ret>::type
get_last_n(T && t, Qargs && ...qargs)
{
    return Ret{std::forward<T>(t), std::forward<Qargs>(qargs)...};
}    

template <std::size_t I, typename Ret, typename T, typename... Qargs>
typename std::enable_if<sizeof...(Qargs)+1  != I, Ret>::type
get_last_n(T && , Qargs && ...qargs)
{
    static_assert(I <= sizeof...(Qargs) + 1, "Not enough parameters to pass to the singal function");
    return get_last_n<I, Ret>(std::forward<Qargs>(qargs)...);
}    

template <typename T, typename... Fargs, 
          typename... Qargs>
void test_signal_daisy_chain(T* t, void(T::*t_signal)(Fargs...),
                             Qargs&&... qargs)
{
    static_assert((sizeof...(Qargs) - sizeof...(Fargs)) % 2 == 0,
                  "Expecting even number of parameters for object-signal pairs");
    auto fargs = get_last_n<sizeof...(Fargs), std::tuple<Fargs...>>(
                             std::forward<Qargs>(qargs)...);
    test_signal_daisy_chain_impl(fargs, t, t_signal, 
                                     std::forward<Qargs>(qargs)...);
}

编辑2

可以通过将所有参数存储在元组中来避免运行时递归。下面的test_signal_daisy_chain_flat()正是这样做的,同时保留与test_signal_daisy_chain()相同的接口:

template <typename Fargs, typename Pairs, std::size_t ...I>
void apply_pairs(Fargs && fargs, Pairs && pairs, const indexes<I...> &)
{
    int dummy[] = {
        (apply_method(std::forward<Fargs>(fargs),
                      std::get<I*2>(pairs),
                      std::get<I*2+1>(pairs)),
         0)...
    };
    (void)dummy;
}
template <typename T, typename... Fargs, 
          typename... Qargs>
void test_signal_daisy_chain_flat(T* t, void(T::*t_signal)(Fargs...),
                                  Qargs&&... qargs)
{
    static_assert((sizeof...(Qargs) - sizeof...(Fargs)) % 2 == 0,
                  "Expecting even number of parameters for object-signal pairs");
    auto fargs = get_last_n<sizeof...(Fargs), std::tuple<Fargs...>>(
                             std::forward<Qargs>(qargs)...);
    std::tuple<T*, void(T::*)(Fargs...), const Qargs&...> pairs{
        t, t_signal, qargs...};
    apply_pairs(fargs, pairs,
                typename make_indexes<(sizeof...(Qargs) - sizeof...(Fargs))/2>
                ::type{});
}

注意事项:

  1. 不断言参数对是否匹配。编译器只是在递归中失败了。
  2. 传递给函数的参数类型是从第一个函数的签名中推导出来的,而不考虑后续参数的类型 - 后续参数将被转换为所需的类型。
  3. 所有函数都需要具有相同的签名。

非常抱歉我没有表达清楚,每个函数(指针)的Fargs是相等的,即signal1、signal2和signal3都具有相同的签名,并且我们将通过根据Fargs传递数据来测试它们的连接。例如,如果函数签名为void(T::*)(int, int, int),我们为我们的Fargs传递三个整数。Sargs对应于可变数量的QObjects及其信号,在这种情况下,它们是成员函数指针,因此Sargs会像QObject,fptr,...,Fargs...)一样被解包。 - jfh
没有什么急事,我最终想要的解决方案需要满足两个标准: - jfh
  1. 具有类似于 method(Object, ptr, Object, ptr, .../* 可变数量的 "Object, ptr", Fargs...) 的风格。
  2. 不需要显式类型声明或包装器参数。
- jfh
1
@jfhcs对代码进行了重大更改以匹配澄清后的需求。现在接受元组的函数名称中包含“_impl”。 - Michael Veksler
1
@jfhcs 我在答案末尾添加了一个非递归变体,名为 test_signal_daisy_chain_flat。我使用了一个新名称来避免混淆,但基本上它具有与 test_signal_daisy_chain 相同的接口。 - Michael Veksler
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接