为什么C++11中的Lambda函数没有关联到function<>类型?

17
我正在尝试使用C++11的函数式特性。其中一个奇怪的事情是,lambda函数的类型实际上不是function<>类型。而且,lambda似乎并不完全适用于类型推断机制。
以下是一个小例子,我在其中测试了将两个整数相加的函数的两个参数交换的情况。(我使用的编译器是MinGW下的gcc 4.6.2) 在此示例中,addInt_f的类型已明确定义为function<>,而addInt_l是使用auto进行类型推断的lambda。
当我编译代码时,flip函数可以接受显式定义类型版本的addInt,但不能接受lambda版本,会出现错误,提示“testCppBind.cpp:15:27: error: no matching function for call to 'flip(<lambda(int, int)>&)'
接下来几行显示,如果将lambda版本(以及“原始”版本)显式强制转换为适当的function<>类型,则可以接受它们。
因此,我的问题是:
1.lambda函数为什么一开始就没有function<>类型?在这个小例子中,为什么addInt_l的类型不是function,而是不同的lambda类型?从函数式编程的角度来看,函数/函数对象和lambda之间有什么区别?
2.如果这两者必须有根本性的不同,那么为什么?我听说lambda可以转换为function<>,但它们是不同的。这是C++11的设计问题/缺陷,还是实现问题,或者区分这两者有好处?似乎addInt_l的类型签名已经提供了关于函数的参数和返回类型足够的信息。
3.有没有一种方法可以编写lambda,使得可以避免上述显式类型转换?
谢谢。
    //-- testCppBind.cpp --
    #include <functional>
    using namespace std;
    using namespace std::placeholders;

    template <typename T1,typename T2, typename T3>
    function<T3 (T2, T1)> flip(function<T3 (T1, T2)> f) { return bind(f,_2,_1);}

    function<int (int,int)> addInt_f = [](int a,int b) -> int { return a + b;};
    auto addInt_l = [](int a,int b) -> int { return a + b;};

    int addInt0(int a, int b) { return a+b;}

    int main() {
      auto ff = flip(addInt_f);   //ok
      auto ff1 = flip(addInt_l);  //not ok
      auto ff2 = flip((function<int (int,int)>)addInt_l); //ok
      auto ff3 = flip((function<int (int,int)>)addInt0);  //ok

      return 0;
    }


4
你不应该使用std::function作为参数,因为它会抑制类型推导(这也是你在这里遇到的问题)。 - R. Martinho Fernandes
1
Lambda表达式被转换为匿名Functor(如果它们不捕获环境,则为函数)。将它们转换为std::function会在语言和库之间引入强耦合,因此这将是一个非常糟糕的想法。 - MFH
@MFH <nitpick> Lambdas总是匿名函数对象。这些函数对象可以转换为函数指针。 - R. Martinho Fernandes
@ MFH。 我认为 lambda 和 function<> 都是在 C++11 中引入的库构造。因此,在理论上,lambda 可以被定义为返回 function<> 对象。我不明白这为什么是不可能的,或者它会如何干扰语言。 - thor
3
你说错了。Lamdas 不是库构造,而是一种新的语言特性。 - MFH
2个回答

41

std::function是一种工具,它可以存储任何类型的可调用对象,而不考虑其类型。为了实现这一点,它需要采用一些类型抹除技术,这涉及到一些开销。

任何可调用对象都可以隐式转换为std::function,这就是为什么它通常可以无缝工作的原因。

我再重复一遍,以确保它变得清晰明了:std::function不仅仅适用于lambda表达式或函数指针:它适用于任何类型的可调用对象。例如,这包括像struct some_callable { void operator()() {} };这样的简单对象,但也可能是像下面这样的复杂对象:

struct some_polymorphic_callable {
    template <typename T>
    void operator()(T);
};

一个Lambda只是另一个可调用对象,类似于上面的some_callable对象实例。它可以存储在std::function中,因为它是可调用的,但它没有std::function的类型擦除开销。
委员会计划将Lambda多态化,即像上面的some_polymorphic_callable那样的Lambda。这样的Lambda将是哪种std::function类型呢?
现在...模板参数推导或隐式转换。选择其中一个。这是C++模板的规则之一。
要将Lambda作为std::function参数传递,它需要被隐式转换。使用std::function参数意味着您选择隐式转换而不是类型推导。但是,您的函数模板需要推导或显式提供签名。
解决方案?不要限制您的调用者只能使用std::function。接受任何类型的可调用对象
template <typename Fun>
auto flip(Fun&& f) -> decltype(std::bind(std::forward<Fun>(f),_2,_1))
{ return std::bind(std::forward<Fun>(f),_2,_1); }

你现在可能会想,为什么我们需要std::function呢?因为std::function提供了已知签名的可调用对象的类型擦除功能。这使得它能够存储类型擦除的可调用对象,并编写虚拟接口。


@TingL 多态 lambda 的类型无法表达,因为 function<> 只接受 一个 签名参数,而多态 lambda 将有 无限数量的不同签名(这就是为什么它被称为多态:它有许多形式)。对于智能指针,开销问题并不完全适用。unique_ptr 真正具有 开销(这是设计目标之一)。如果您没有运行多线程程序,则 shared_ptr 可能会有一些开销,但多线程程序是更可能使用它的情况。 - R. Martinho Fernandes
我的示例中的decltype并不是必需的。我使用它是因为它可以省去我自己计算返回类型的麻烦。如果函数有很多行,你可以只放置像在decltype中返回的表达式之类的东西(毕竟这才是计算返回类型所需要的全部内容)。但有时候情况会变得有点棘手 :( - R. Martinho Fernandes
我非常理解你对于捕获错误的担忧。不幸的是,旨在解决这个问题的概念提案并没有准备好,也没有包含在标准的这个版本中。不过,目前你可以使用 static_assertis_callable 特性来获取良好的错误提示(遗憾的是它们不在标准库中,所以你需要自己编写)。 - R. Martinho Fernandes
这个话题有点冗长了,而且现在是我所在时区的睡觉时间 :). 如果还有什么不清楚的地方,你可以稍后来聊天室,那里更适合这种事情。我通常在欧洲白天会在那里。 - R. Martinho Fernandes
顺便说一句,我尝试为function<>添加类型转换运算符(以小的开销为代价),因为我无法容忍为这样简单的事情所需的语法复杂性;但显然,这似乎是不可能的。有关如何启用自动转换(从lambda到function<>)的任何指针吗? - thor
显示剩余4条评论

5
  1. 因为function<>使用类型擦除。这允许在function<>中存储多个不同的函数类型,但会产生一些运行时开销。类型擦除隐藏了实际类型(您特定的lambda)在虚函数接口后面。
  2. 这样做有好处:C++设计的一个“公理”是除非真正需要,否则不要增加开销。使用这种设置,当使用类型推断(使用auto或作为模板参数传递)时,您没有任何开销,但仍然具有通过function<>与非模板代码交互的所有灵活性。还要注意,function<>不是语言结构,而是标准库的一个组件,可以使用简单的语言特性来实现。
  3. 不行,但是你可以编写函数只接受函数类型(语言结构),而不是function<>的具体细节(库结构)。当然,这使得实际编写返回类型变得更加困难,因为它不直接给出参数类型。但是,使用一些元编程,比如Boost.FunctionTypes,您可以从传递的函数中推导出这些参数类型。但也有一些无法实现这一点的情况,例如具有模板operator()的函数对象。

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