为什么C++11不会隐式地将lambda转换为std::function对象?

20
我实现了一个通用的事件发射器类,它允许代码注册回调函数并带有参数发射事件。我使用了 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;
}
3个回答

17
一个 Lambda 表达式不是一个 std::function,而 std::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(),并存储一个智能值语义指向抽象接口的指针,该接口涵盖拷贝/移动/调用操作。它具有一个 template 的构造函数,可以接受任何支持拷贝/移动/带有兼容签名的 operator() 的类型,生成实现抽象内部接口的具体自定义类,并将其存储在上述内部值语义智能指针中。
然后,它将自身作为值类型转发到抽象内部指针上进行操作,包括完美转发到调用方法。
正如事实所证明的那样,您可以将 lambda 存储在 std::function 中,就像您可以存储函数指针一样。
但是有许多不同的 std::function 可以存储给定的 lambda——任何可转换成参数并从参数转换回来的类型都可以,并且实际上都可以工作,就 std::function 而言。
在 C++ 中,template 的类型推导并不适用于 "是否可以转换为" 这个层次——它只是纯粹的模式匹配。由于 lambda 是与任何 std::function 无关的类型,因此不能从中推导出任何 std::function 类型。
在一般情况下,如果 C++ 尝试这样做,它将不得不反转图灵完备过程,以确定可以传递给 template 的哪个(如果有)一组类型可生成一个兼容的实例。
理论上,我们可以向语言中添加 "从中推导出模板参数的运算符",其中给定 template 的实现者可以编写代码,接受某些任意类型,并尝试提取 "从该类型中,应使用哪些 template 参数的实例"。但是,C++ 没有这样的运算符。

看起来C++有其他语言没有的问题。在Haskell、C#和Python中,你有一个lambda类型,不需要任何显式转换。 - Trismegistos
1
@trismegistos 是的,这些语言默认都会擦除lambda类型。C++ lambda旨在与编写合理优化的C级代码一样快,但更容易传递给算法,这意味着lambda的类型信息不会被丢弃,因此使用它们的算法会为每个传递的lambda复制并区分,这使得内联变得微不足道。 - Yakk - Adam Nevraumont
这是否意味着,当我传递两次创建的相同lambda,并在这两个lambda上调用std :: find_if时,将编译出两个不同的find_if函数,它们是二进制相同的?这听起来不像是很好的优化。 - Trismegistos
1
@trismegistos 是的,有两个。如果在 as-if 条件下(即结果函数的地址)从未被使用,则可以消除重复项。MSVC 将其称为 COMDAT 折叠。(如果地址被占用,则按照标准地址必须不同(某些编译器是否违反此规定?),但是通过 goto 子程序可以使主体相同)。同样,相同 lambda 的两个闭包实例当然是相同的 lambda。 - Yakk - Adam Nevraumont

5
编译器不会推断任何东西,因为编译器实现的是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有关。或者我错了吗? - templatetypedef
在我看来,编译器应该能够推断出T是一个int类型,因为这个信息在编译时显然是存在的。你是说它就是不能推断出来,然后就没了吗? - gcv
5
为什么不使用 Foo<char> 或者 Foo<void>?每个可能的 Foo 实例都有一个从 int 转换的构造函数。 - Casey

1
boost::any 存储一个值时,它使用该对象的静态类型来确定正在存储的对象类型。如果您指定正在存储的静态类型,则只能将 any 强制转换回正确类型的对象。
每个 C++ lambda 与用户不透明的实现定义类型相关联。尽管 lambda 可以作为函数调用,但它们不直接求值为 std::function。在将 lambda 存储在 any 中时,需要进行强制转换,以确保所存储的静态类型是 std::function,以便在强制转换回来时使用。
希望这有所帮助!

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接