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

Variadic helper function with partial argument pack

本文关键字:参数 变参 函数      更新时间:2023-10-16

在下面的代码中:

#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 可以在没有任何模板参数的情况下推断它们。 在这里完全困惑。 两个编译器都在这里出错了吗? 程序员方面有什么可能的修复吗?

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

编译器无法从给定的参数列表中推断出(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);

我认为这两个调用都是无效的,因为两者都涉及非推导上下文。从 §14.8.2.5 开始:

非推导上下文是:

— [ . ]

— 不出现在参数声明列表末尾的函数参数包。

当以包含非推导上下文的方式指定类型名称时,构成该类型名称的所有类型也是非推导的。

当你有 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...放在一个非推导的上下文中 - 并且标准对于之后应该发生的事情是明确的,特别是考虑到 [temp.deduct.call]/p1 中的规则

当函数参数包出现在非推导上下文中时 (14.8.2.5),永远不会推断出该参数包的类型。

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

即使在正常情况下,您也必须编写大约 12 个重载来匹配所有可能形式的成员函数指针(4 个可能的 cv-qualifier-seqs 乘以 3 个可能的 ref 限定符)。在您的情况下,跳过一些(例如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 必须是一个函数,可以在 std::is_function<R>::valuestatic_assert

演示。