【问题标题】:Why doesn't C++11 implicitly convert lambdas to std::function objects?为什么 C++11 不将 lambda 隐式转换为 std::function 对象?
【发布时间】:2014-03-02 10:41:03
【问题描述】:

我实现了一个通用的事件发射器类,它允许代码注册回调,并发出带有参数的事件。我使用 Boost.Any 类型擦除来存储回调,以便它们可以具有任意参数签名。

一切正常,但由于某种原因,传入的 lambda 必须首先转换为 std::function 对象。为什么编译器不推断 lambda 是函数类型?是因为我使用可变参数模板的方式吗?

我使用 Clang(版本字符串:Apple LLVM version 5.0 (clang-500.2.79) (based on LLVM 3.3svn))。

代码:

#include <functional>
#include <iostream>
#include <map>
#include <string>
#include <vector>

#include <boost/any.hpp>


using std::cout;
using std::endl;
using std::function;
using std::map;
using std::string;
using std::vector;


class emitter {

   public:

      template <typename... Args>
      void on(string const& event_type, function<void (Args...)> const& f) {
         _listeners[event_type].push_back(f);
      }

      template <typename... Args>
      void emit(string const& event_type, Args... args) {
         auto listeners = _listeners.find(event_type);
         for (auto l : listeners->second) {
            auto lf = boost::any_cast<function<void (Args...)>>(l);
            lf(args...);
         }
      }

   private:

      map<string, vector<boost::any>> _listeners;

};


int main(int argc, char** argv) {

   emitter e;

   int capture = 6;

   // Not sure why Clang (at least) can't deduce the type of the lambda. I don't
   // think the explicit function<...> business should be necessary.
   e.on("my event",
        function<void ()>( // <--- why is this necessary?
           [&] () {
              cout << "my event occurred " << capture << endl;
           }));
   e.on("my event 2",
        function<void (int)>(
           [&] (int x) {
              cout << "my event 2 occurred: " << x << endl;
           }));
   e.on("my event 3",
        function<void (double)>(
           [&] (double x) {
              cout << "my event 3 occurred: " << x << endl;
           }));
   e.on("my event 4",
        function<void (int, double)>(
           [&] (int x, double y) {
              cout << "my event 4 occurred: " << x << " " << y << endl;
           }));

   e.emit("my event");
   e.emit("my event 2", 1);
   e.emit("my event 3", 3.14159);
   e.emit("my event 4", 10, 3.14159);

   return EXIT_SUCCESS;
}

【问题讨论】:

    标签: c++ templates c++11 lambda template-function


    【解决方案1】:

    lambda 不是 std::functionstd::function 不是 lambda。

    lambda 是创建匿名类的语法糖,如下所示:

    struct my_lambda {
    private:
      int captured_int;
      double captured_double;
      char& referenced_char;
    public:
      int operator()( float passed_float ) const {
        // code
      }
    };
    int captured_int = 7;
    double captured_double = 3.14;
    char referenced_char = 'a';
    my_lambda closure {captured_int, captured_double, referenced_char};
    closure( 2.7f );
    

    从此:

    int captured_int = 7;
    double captured_double = 3.14;
    char referenced_char = 'a';
    auto closure = [=,&referenced_char](float passed_float)->int {
      // code
    };
    closure(2.7);
    

    my_lambda 的类型名称实际上是一些不可命名的类型。

    std::function 是完全不同的东西。它是一个使用特定签名实现operator() 的对象,并将智能值语义指针存储到涵盖复制/移动/调用操作的抽象接口。它有一个templated 构造函数,可以采用任何支持复制/移动/operator() 并具有兼容签名的类型,生成一个实现抽象内部接口的具体自定义类,并将其存储在上述内部值语义中智能指针。

    然后它将操作作为值类型从自身转发到抽象内部指针,包括完美转发到调用方法。

    碰巧,您可以将 lambda 存储在 std::function 中,就像您可以存储函数指针一样。

    但是有无数不同的std::function 可以存储给定的 lambda -- anything 其中类型可以转换为参数并从参数有效,实际上同样有效,因为就std::function而言。

    templates 中的 C++ 类型推导在“你可以转换成”级别上不起作用——它是模式匹配,纯粹而简单。由于 lambda 是与任何 std::function 无关的类型,因此无法从中推导出 std::function 类型。

    如果 C++ 在一般情况下尝试这样做,则必须反转图灵完备过程以确定可以将哪些(如果有)类型集传递给 template 以生成转换兼容实例。

    理论上,我们可以在语言中添加“运算符推导模板参数”,给定template 的实现者可以编写采用任意类型的代码,然后他们试图梳理出“从这种类型,什么template 参数应该用于实例”。但是 C++ 没有这个。

    【讨论】:

    • 看起来 C++ 有其他任何语言都没有的问题。在 Haskell、C#、Python 中,您有一个 lambda 类型,您不需要任何显式转换。
    • @trismegistos 是的,这些语言默认都键入擦除 lambda。 C++ lambda 被设计为与编写合理优化的 C 级代码一样快,但在传递给算法时要容易得多,这意味着 lambda 的类型信息不会被丢弃,因此使用它们的算法对于每个 lambda 都是复制和不同的通过,这使得内联变得微不足道。
    • 这是否意味着当我通过两次创建相同的 lamdba 并在两个 lambda 上调用 std::find_if 时,我将编译两个不同的 find_if 函数,它们是二进制相同的?这听起来不像是好的优化。
    • @trismegistos 是的,有两个。在仿佛从未使用过(结果函数的)地址的情况下,可以消除重复项。 MSVC 称之为 COMDAT 折叠。 (如果采用地址,则按照标准,地址必须不同(某些编译器违反了这一点?),但主体可以通过 goto 存根相同)。当然,同一个 lambda 的两个闭包实例也是同一个 lambda。
    【解决方案2】:

    编译器不会推断任何东西,因为编译器实现了 C++ 语言,而该语言的模板参数推导规则不允许以您想要的方式进行推导。

    这是一个代表您的情况的简单示例:

    template <typename T> struct Foo
    {
        Foo(int) {}
    };
    
    template <typename T> void magic(Foo<T> const &);
    
    int main()
    {
        magic(10);   // what is T?
    }
    

    【讨论】:

    • 我认为这解释了为什么不能将 lambda 直接传递给采用模板化函数的方法,但我认为这里的根本问题与any 的类型推导规则有关比std::function。还是我弄错了?
    • 在我看来,编译器当然应该推断 T 是一个 int,因为该信息在编译时明确存在。你是说它只是没有,那就结束了吗?
    • @gcv 为什么不Foo&lt;char&gt;?或Foo&lt;void&gt;Foo 的每个可能的实例化都有一个来自 int 的转换构造函数。
    【解决方案3】:

    boost::any 存储一个值时,它使用该对象的静态类型来确定存储的对象类型。然后,如果您指定所存储内容的静态类型,则只能将 any 转换回正确类型的对象。

    每个 C++ lambda 都与对用户不透明的实现定义类型相关联。尽管 lambdas 可以作为函数调用,但它们不会直接计算为 std::functions。将 lambda 存储在 any 中时需要进行强制转换,以确保存储的静态类型是 std::function,以便在您进行转换时使用。

    希望这会有所帮助!

    【讨论】:

      猜你喜欢
      • 2022-01-23
      • 1970-01-01
      • 2017-04-22
      • 1970-01-01
      • 1970-01-01
      • 2020-10-28
      • 2013-02-03
      相关资源
      最近更新 更多