带有部分参数包的可变辅助函数

Variadic helper function with partial argument pack

在下面的代码中:

#include <iostream>

struct Base {
    virtual ~Base() = default;
    template <typename T, typename... Args> void helper (void (T::*)(Args..., int), Args...);
    void bar (int n) {std::cout << "bar " << n << std::endl;}
};

struct Derived : Base {
    void baz (double d, int n) {std::cout << "baz " << d << ' ' << n << std::endl;}
};

template <typename T, typename... Args>
void Base::helper (void (T::*f)(Args..., int), Args... args) {
    // A bunch on lines here (hence the motivation for the helper function)
    for (int n = 0;  n < 5;  n++)
        (dynamic_cast<T*>(this)->*f)(args..., n);
    // ...
}

int main() {
    Base b;
    Derived d;
    b.helper(&Base::bar);  // GCC 4.8.1 will accept this, Visual Studio 2013 won't.
    d.helper<Derived, double>(&Derived::baz, 3.14);  // Visual Studio 2013 will accept this, GCC 4.8.1 won't
}

我无法让 GCC4.8.1 或 VS2013 编译以上两行。他们将只编译一个而不编译另一个(而且他们也不同意哪一行是正确的和不正确的)。错误消息表明两个编译器都失败了模板推导。那么到底出了什么问题呢?我把所有的模板参数都放在最后一行(我认为这是可以推导的),但它仍然不能被 GCC 推导,尽管 VS 可以。然而,当我为此放置模板参数时,VS 无法推断出 b.foo(&Base::bar); 行的模板参数,但 GCC 可以在没有任何模板参数的情况下推断出它们。到这里完全懵了。两个编译器都在这里被窃听了吗?程序员方面有什么可能的修复方法吗?

我认为这两个调用都是无效的,因为它们都涉及非推导上下文。来自 §14.8.2.5:

The non-deduced contexts are:

— [ .. ]

— A function parameter pack that does not occur at the end of the parameter-declaration-list.

When a type name is specified in a way that includes a non-deduced context, all of the types that comprise that type name are also non-deduced.

当你有 void (T::*f)(Args..., int) 时,那是在非推导上下文中,因为函数内部的函数参数包不会出现在最后。指向成员的参数列表是非推导的这一事实使得整个函数调用是非推导的。因此不能推导出这个调用:

b.helper(&Base::bar);

对于第二个,即使看起来您明确指定了 Args...,参数 void (T::*f)(Args..., int) 仍然处于非推导上下文中,因此编译器无法知道是否需要更多 Args

因此,一个解决方案是强制不必推导该论点,例如,通过使用向后的身份技巧:

template <typename T, typename... Args> 
void foo (void (T::*)(typename identity<Args>::type..., int), Args...);

这样,这两行都可以编译:

b.helper(&Base::bar);
d.helper<Derived, double>(&Derived::baz, 3.14);

尽管现在您必须确保 Args... 完全正确,如果您没有明确指定的话。

参数包必须放在参数列表的末尾才能自动推导。

编译器无法从给定的参数列表中推导出 (Args..., int),请改用 (int, Args...),程序将编译。

#include <iostream>

struct Base {
    virtual ~Base() = default;
    template <typename T, typename... Args> void helper (void (T::*)(int, Args...), Args...);
    void bar (int n) {std::cout << "bar " << n << std::endl;}
};

struct Derived : Base {
    void baz (int n, double d) {std::cout << "baz " << d << ' ' << n << std::endl;}
};

template <typename T, typename... Args>
void Base::helper (void (T::*f)(int, Args...), Args... args) {
    // A bunch on lines here (hence the motivation for the helper function)
    for (int n = 0;  n < 5;  n++)
        (dynamic_cast<T*>(this)->*f)(n, args...);
    // ...
}

int main() {
    Base b;
    Derived d;
    b.helper(&Base::bar);
    d.helper<Derived, double>(&Derived::baz, 3.14);
}

如果你必须把 int 放在参数列表的末尾,你可以像@Barry 说的那样使用 identity 技巧。

准系统 identity 实现可以像这样简单:

template<typename T>
struct identity {
    typedef T type;
};

然后你可以手动推断参数类型:

template <typename T, typename... Args>
void Base::helper (void (T::*f)(typename identity<Args>::type..., int), typename identity<Args>::type... args) {
    // A bunch on lines here (hence the motivation for the helper function)
    for (int n = 0;  n < 5;  n++)
        (dynamic_cast<T*>(this)->*f)(args..., n);
    // ...
}

b.helper<Base>(&Base::bar);
d.helper<Derived, double>(&Derived::baz, 3.14);

我根本不会将第一个参数写成指向成员函数的指针。

在您的特定情况下,它需要将第一个 Args... 放入非推导的上下文中 - 并且该标准对于之后应该发生的事情非常清楚,特别是考虑到 [temp.deduct.call]/p1 即

When a function parameter pack appears in a non-deduced context (14.8.2.5), the type of that parameter pack is never deduced.

我不知道当您改写 void (T::*)(typename identity<Args>::type..., int) 时这条规则的含义是什么。编译器也不同意彼此。

即使在正常情况下,您也必须编写大约 12 个重载来匹配所有可能形式的成员函数指针(4 个可能的 cv-qualifier-seqs 乘以 3可能 ref-qualifiers)。在你的情况下,跳过一些(例如 volatile&&)可能是安全的,但它仍然是烦人的代码重复。此外,如果您在推导上下文中使用 Args... 两次,它们将被独立推导并且推导类型必须完全匹配,这对最终用户来说可能会很混乱。 (std::max(1, 2.5),有人吗?)

相反,我会把它写成指向成员的指针:

template <typename T, typename... Args, typename R>
void Base::helper (R T::*f, Args... args) {
    // A bunch of lines here (hence the motivation for the helper function)
    for (int n = 0;  n < 5;  n++)
        (dynamic_cast<T*>(this)->*f)(args..., n);
    // ...
}

R T::* 匹配所有指向成员的指针;当您将指针传递给成员函数时, R 被推断为函数类型。如果你想强制执行 R-must-be-a-function,你可以 static_assert on std::is_function<R>::value.

Demo.