【问题标题】:Pass template functor to a template std::function将模板函子传递给模板 std::function
【发布时间】:2015-05-01 12:53:22
【问题描述】:

这是一个代码sn-p:

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

template <typename T>
using Callback = std::function<void(const T)>;

template <typename T>
void Call(const T yyy, const Callback<T>& callback) {
    callback(yyy);
}

template <typename T>
class MyCallback {
public:
    explicit MyCallback(const T counter_init) : counter_{counter_init} {}
    void operator ()(const T xxx) {
        std::cout << counter_ << '\t' << xxx << std::endl;
        ++counter_;
    }
private:
    T counter_;
};

int main() {
    const auto callback = std::make_unique<MyCallback<int>>(0);
    Call(111, *callback);  // expected output is "0 \t 111"
    Call(222, *callback);  // expected output is "1 \t 222"
    return 0;
}

Clang 说它无法将 std::functionMyCallback 匹配,而 g++ 认为 MyCallback 是从 Callback 派生的.

clang++ -std=c++14 main.cpp && ./a.out

g++ -std=c++14 main.cpp && ./a.out

我知道解决此问题的最简单方法是使用模板而不是 Callback,因此 Call 将按以下方式定义:

template <typename T, typename F>
void Call(const T yyy, F&& callback) {
    callback(yyy);
}

但是对于下一个开发者来说,callback 有哪个签名就不清楚了。

有人能从编译器的角度澄清第一个示例中发生了什么吗?如何在不应用我上面描述的 hack 的情况下解决这个问题?

【问题讨论】:

  • std::function&lt;void(const T)&gt;&amp;&amp;F&amp;&amp; 之间有一个根本区别:第一个强制使用std::function,这是一个多态函数包装器 并且可能有开销。
  • 为什么首先要移动回调?将其作为参考有什么问题?
  • 如果仅用于文档目的,我建议不要使用std::function,因为它可能会产生开销。例如,您可以将第二个参数限制为 callable 作为一些 void(T) 签名。这看起来像这样:template&lt;typename T, typename F, REQUIRE( is_callable&lt;F, void(T)&gt; )&gt; void Call(T const yyy, F&amp;&amp; callback)。也可以仅出于文档目的将其编写为template&lt;typename T, typename F&gt; void Call(const T yyy, callable&lt;F, void(T)&gt;&amp;&amp; callback),无需任何检查,使用template&lt;typename T, typename&gt; using callable = T;
  • @dyp,@david-haim,从 Call 中删除了右值引用。
  • @KostyaBazhanov 我的第一个(现已删除)评论是错误的,在您的示例中它是 const Callback&lt;T&gt;&amp; 还是 Callback&lt;T&gt;&amp;&amp; 都没有关系:在任何一种情况下,使用像 @987654336 这样的左值调用它@ 将创建一个绑定到函数参数的临时std::function(用左值初始化)。 -- const&amp; 参数将允许使用 std::function 类型的左值调用它。

标签: c++ templates c++11 c++14


【解决方案1】:

Call 更改为:

template <class T, class F,
  class R=typename std::result_of<F&(const T&)>::type
>
void Call(const T& yyy, F&& f) {
  f(yyy);
}

现在我们在yyy 上调用f,并且(假设您有一个实现SFINAE 安全result_of 的C++11 编译器)重载仅在您可以在yyy 上调用f 时才有效。

std::function 不是通用的可调用对象。这是一种将可调用对象类型擦除到“使用给定签名调用”、复制可调用对象并销毁可调用对象的方法。

类型擦除和类型推断是相反的操作。一步完成两者通常是设计缺陷的标志。 std::function 应该很少从传入的可调用的签名中推断出来。

相反,确定您将如何使用给定的功能。然后键入擦除到该使用签名,或者仅针对该使用签名进行测试并且根本不键入擦除。

如果您有多个可能的使用签名,请针对每一个进行测试,标记调度到正确的类型擦除路径,然后在此处键入擦除。

result_of 子句是可选的,但它显着改进了错误消息。它还使错误可以“及早”检测为失败的过载,而不是硬错误。 result_of 子句可以在正文中转换为 static_assert,这将生成更清晰的错误消息,但在重载解决后会“延迟”失败。

阻止对function 进行扣除的另一种方法是:

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

template <typename T>
using Callback = block_deduction<std::function<void(const T)>>;

现在

template <class T>
void Call(const T& yyy, const Callback<T>& callback) {
  callback(yyy);
}

有效。它仍然会不必要地删除 callback 类型(产生开销)。

通过struct 往返并获得::type 来阻止推理。在标准下,这种依赖类型永远不会被推导出来。

【讨论】:

    【解决方案2】:

    std::function&lt;void(const T)&gt; 可以用*callback 进行值构造。问题是Call 是一个函数模板。编译器无法从Call(111, *callback); 自动实例化void Call(const int yyy, const Callback&lt;int&gt;&amp; callback),因为它与调用不匹配。必须进行转换才能使用Call(111, *callback); 调用实例化。但是自动模板参数推导不考虑这种转换。它只考虑完全匹配。你可以手动实例化它。因此,纠正代码的一种方法是更改​​

    Call(111, *callback);  // expected output is "0 \t 111"
    Call(222, *callback);  // expected output is "1 \t 222"
    

    Call<int>(111, *callback);  // expected output is "0 \t 111"
    Call<int>(222, *callback);  // expected output is "1 \t 222"
    

    因此,如果第一个值是用*callback 构造的,则为临时的std::function&lt;void(const T)&gt;。然后将Call 中的函数参数const Callback&lt;int&gt;&amp; callback 绑定到这个临时变量。同样,这都是关于转换的。函数签名与调用不完全匹配。

    【讨论】:

    • 因为void Call(const int yyy, const Callback&lt;int&gt;&amp; callback) 与调用Call(111, *callback); 不匹配。这里涉及转换。扣除过程不考虑转化。
    【解决方案3】:

    来自 [temp.arg.explicit]:

    隐式转换(第 4 条)将对函数参数执行以将其转换为 对应的函数参数如果参数类型不包含参与的模板参数 在模板实参推导中。 [注:模板形参不参与模板实参 如果明确指定,则扣除。例如,

    template<class T> void f(T);
    
    class Complex {
        Complex(double);
    };
    
    void g() {
        f<Complex>(1); // OK, means f<Complex>(Complex(1))
    }
    

    ——尾注]

    在您的情况下,const Callback&lt;T&gt;&amp; 确实包含一个参与模板参数推导的 模板参数,因此不允许隐式转换(即 MyCallback&lt;int&gt;std::function&lt;void(const int)&gt;)。

    为了使用std::function,您必须让callback 参数不参与任何模板参数推导。那就是:

    Call<int>(111, *callback); // no argument deduction done here
    

    或者,您可以只推断 callback 本身,不需要类型擦除:

    template <typename T, typename F>
    void Call(const T yyy, F&& callback) {
        callback(yyy);
    }
    

    最后,如果你真的想要类型擦除,你可以在内部手动将Callback 构造为Call

    template <typename T, typename F>
    void Call(const T yyy, F&& f) {
        Callback<T> callback(std::forward<F>(f));
        callback(yyy);
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-12-17
      • 1970-01-01
      • 2017-06-09
      • 2019-02-15
      • 2017-12-25
      • 2018-03-07
      • 2016-06-03
      相关资源
      最近更新 更多