【问题标题】:Variadic helper function with partial argument pack带有部分参数包的可变辅助函数
【发布时间】:2015-03-14 14:41:03
【问题描述】:

在以下代码中:

#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(&amp;Base::bar); 行的模板参数,但 GCC 可以在没有任何模板参数的情况下推断它们。完全被这里弄糊涂了。这两个编译器都被窃听了吗?程序员有什么可能的解决办法吗?

【问题讨论】:

  • 有些东西告诉我这两行都是无效的,但我还想不出一个原因。
  • @Barry。我希望你是对的。然后找出 main() 中正确的两行将解决问题,而不必担心编译器的任何错误。
  • 完美转发错误:一转发双扣。我认为其中一种用途是不可推论的。
  • @Yakk。也许吧,但我把完美的转发只是为了完整性。当我删除完美转发时,我仍然得到完全相同的编译错误。
  • 删除完美转发 -- 取值const&amp; 或值。方法指针的块推导(使用常用技术)。显式传递类类型。

标签: c++ templates c++11 variadic-templates variadic


【解决方案1】:

参数包必须放在参数列表的末尾,以便自动推导。

编译器无法从给定的参数列表中推断出(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... 在这里是否仍然是错误的(作为部分参数包)。虽然我会听从 Yakk 的建议不要这样做。其他人对此有何看法?现在一切似乎都很好,完美转发(经过测试),但我不知道是否真的很好。
【解决方案2】:

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

未推断的上下文是:

——[..]

——一个函数参数包,不出现在parameter-declaration-list的末尾。

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

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

b.helper(&Base::bar);

对于第二个,尽管看起来您明确指定了Args...,但参数void (T::*f)(Args..., int) 仍然处于非推导上下文中,因此编译器无法知道更多 em> 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... 已经在 void (T::*f)(Args..., int) 中的非推断上下文中,那么为什么将它放在另一层非推断上下文中会有所帮助呢?
  • 我真的不知道这里发生了什么。另请注意,[temp.deduct.call]/p1 中的函数参数包有一条特殊规则:“当函数参数包出现在非推导上下文 (14.8.2.5) 中时,永远不会推导该参数包的类型。”
  • @T.C.好吧,如果它从未推断出来,那么至少我的答案的前半部分是正确的——但我应该加上那句话。
  • 好吧,仅仅因为某些部分未演绎并不意味着整个函数调用的演绎失败,并且仅仅因为涉及未演绎的上下文并不意味着函数调用无效。整个identity&lt;T&gt; 技巧是基于防止从一个参数中推断出T,同时仍然允许从另一个参数中推断出它。
  • @T.C.是否应该将此行为报告为编译器错误?当然,标准在这个主题上的措辞有很多模棱两可的地方,但我不明白这怎么可能是预期的行为。也就是说,无论是否使用 identity 包装器,这种特定情况都应该以相同的方式工作。你怎么看?
【解决方案3】:

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

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

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

当您改为写void (T::*)(typename identity&lt;Args&gt;::type..., int) 时,我不知道这条规则的含义是什么。编译器之间也存在分歧。

即使在正常情况下,您也必须编写大约 12 个重载来匹配所有可能形式的成员函数指针(4 个可能的 cv-qualifier-seqs 乘以 3 个可能的 ref -限定符)。在您的情况下,跳过一些(例如volatile&amp;&amp;)可能是安全的,但它仍然是令人讨厌的代码重复。另外,如果你在推导上下文中使用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,您可以在 std::is_function&lt;R&gt;::valuestatic_assert

Demo.

【讨论】:

  • 一个较晚的答案,但这表明很少使用的指向数据成员的指针的明确用途。
  • 完美转发问题呢?让Derived 具有重载void baz (double&amp; d, int n) {std::cout &lt;&lt; "baz, double&amp;\n";}void baz (double&amp;&amp; d, int n) {std::cout &lt;&lt; "baz, double&amp;&amp;\n";}。如何处理?
猜你喜欢
  • 2011-08-24
  • 1970-01-01
  • 1970-01-01
  • 2016-01-08
  • 1970-01-01
  • 2016-03-30
  • 2021-10-30
  • 1970-01-01
  • 2019-08-02
相关资源
最近更新 更多