添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
C++ lambda表达式与函数对象

lambda 表达式是 C++11 中引入的一项新技术,利用 lambda 表达式可以编写内嵌的匿名函数,用以替换独立函数或者函数对象,并且使代码更可读。但是从本质上来讲, lambda 表达式只是一种语法糖,因为所有其能完成的工作都可以用其它稍微复杂的代码来实现。但是它简便的语法却给 C++ 带来了深远的影响。如果从广义上说, lamdba 表达式产生的是函数对象。在类中,可以重载函数调用运算符 () ,此时类的对象可以将具有类似函数的行为,我们称这些对象为函数对象(Function Object)或者仿函数(Functor)。相比 lambda 表达式,函数对象有自己独特的优势。下面我们开始具体讲解这两项黑科技。

lambda表达式

我们先从简答的例子开始,我们定义一个可以输出字符串的 lambda 表达式,表达式一般都是从方括号 [] 开始,然后结束于花括号 {} ,花括号里面就像定义函数那样,包含了 lamdba 表达式体:

大家可能会想 lambda 表达式最前面的方括号的意义何在?其实这是 lambda 表达式一个很要的功能,就是闭包。这里我们先讲一下 lambda 表达式的大致原理:每当你定义一个 lambda 表达式后,编译器会自动生成一个匿名类(这个类当然重载了 () 运算符),我们称为闭包类型(closure type)。那么在运行时,这个 lambda 表达式就会返回一个匿名的闭包实例,其实一个右值。所以,我们上面的 lambda 表达式的结果就是一个个闭包。闭包的一个强大之处是其可以通过传值或者引用的方式捕捉其封装作用域内的变量,前面的方括号就是用来定义捕捉模式以及变量,我们又将其称为 lambda 捕捉块。看下面的例子:

你可能会想 a b 对应的函数类型是一致的(编译器也显示是相同类型:lambda [] void () -> void),为什么不能相互赋值呢?因为禁用了赋值操作符:

ClosureType& operator=(const ClosureType&) = delete;

但是没有禁用复制构造函数,所以你仍然可以用一个 lambda 表达式去初始化另外一个 lambda 表达式而产生副本。并且 lambda 表达式也可以赋值给相对应的函数指针,这也使得你完全可以把 lambda 表达式看成对应函数类型的指针。

闲话少说,归入正题,捕获的方式可以是引用也可以是复制,但是具体说来会有以下几种情况来捕获其所在作用域中的变量:

  • []:默认不捕获任何变量;
  • [=]:默认以值捕获所有变量;
  • [&]:默认以引用捕获所有变量;
  • [x]:仅以值捕获x,其它变量不捕获;
  • [&x]:仅以引用捕获x,其它变量不捕获;
  • [=, &x]:默认以值捕获所有变量,但是x是例外,通过引用捕获;
  • [&, x]:默认以引用捕获所有变量,但是x是例外,通过值捕获;
  • [this]:通过引用捕获当前对象(其实是复制指针);
  • [*this]:通过传值方式捕获当前对象;

在上面的捕获方式中,注意最好不要使用 [=] [&] 默认捕获所有变量。首先说默认引用捕获所有变量,你有很大可能会出现悬挂引用(Dangling references),因为引用捕获不会延长引用的变量的声明周期:

尽管还是以值方式捕获,但是捕获的是指针,其实相当于以引用的方式捕获了当前类对象,所以 lambda 表达式的闭包与一个类对象绑定在一起了,这也很危险,因为你仍然有可能在类对象析构后使用这个 lambda 表达式,那么类似“悬挂引用”的问题也会产生。所以,采用默认值捕捉所有变量仍然是不安全的,主要是由于指针变量的复制,实际上还是按引用传值。

通过前面的例子,你还可以看到 lambda 表达式可以作为返回值。我们知道 lambda 表达式可以赋值给对应类型的函数指针。但是使用函数指针貌似并不是那么方便。所以 STL 定义在 <functional> 头文件提供了一个多态的函数对象封装 std::function ,其类似于函数指针。它可以绑定任何类函数对象,只要参数与返回类型相同。如下面的返回一个bool且接收两个int的函数包装器:

std::function<bool(int, int)> wrapper = [](int x, int y) { return x < y; };

lambda 表达式一个更重要的应用是其可以用于函数的参数,通过这种方式可以实现回调函数。其实,最常用的是在 STL 算法中,比如你要统计一个数组中满足特定条件的元素数量,通过 lambda 表达式给出条件,传递给 count_if 函数:

第一个是完整的语法,后面3个是可选的语法。这意味着 lambda 表达式相当灵活,但是照样有一定的限制,比如你使用了拖尾返回类型,那么就不能省略参数列表,尽管其可能是空的。针对完整的语法,我们对各个部分做一个说明:

  • capture-list :捕捉列表,这个不用多说,前面已经讲过,记住它不能省略;
  • params :参数列表,可以省略(但是后面必须紧跟函数体);
  • mutable :可选,将 lambda 表达式标记为 mutable 后,函数体就可以修改传值方式捕获的变量;
  • constexpr :可选,C++17,可以指定 lambda 表达式是一个常量函数;
  • exception :可选,指定 lambda 表达式可以抛出的异常;
  • attribute :可选,指定 lambda 表达式的特性;
  • ret :可选,返回值类型;
  • body :函数执行体。

如果想了解更多,可以参考 cppreference lambda

lambda新特性

C++14 中, lambda 又得到了增强,一个是泛型 lambda 表达式,一个是 lambda 可以捕捉表达式。这里我们对这两项新特点进行简单介绍。

lambda捕捉表达式

前面讲过, lambda 表达式可以按复制或者引用捕获在其作用域范围内的变量。而有时候,我们希望捕捉不在其作用域范围内的变量,而且最重要的是我们希望捕捉右值。所以 C++14 中引入了表达式捕捉,其允许用任何类型的表达式初始化捕捉的变量。看下面的例子:

函数对象是一个广泛的概念,因为所有具有函数行为的对象都可以称为函数对象。这是一个高级抽象,我们不关心对象到底是什么,只要其具有函数行为。所谓的函数行为是指的是可以使用 () 调用并传递参数:

function(arg1, arg2, ...);   // 函数调用

这样来说, lambda 表达式也是一个函数对象。但是这里我们所讲的是一种特殊的函数对象,这种函数对象实际上是一个类的实例,只不过这个类实现了函数调用符 ()

泛型提供了高级抽象,不论是 lambda 表达式、函数对象,还是函数指针,都可以传入 for_each 算法中。

本质上,函数对象是类对象,这也使得函数对象相比普通函数有自己的独特优势:

  • 函数对象带有状态 :函数对象相对于普通函数是“智能函数”,这就如同智能指针相较于传统指针。因为函数对象除了提供函数调用符方法,还可以拥有其他方法和数据成员。所以函数对象有状态。即使同一个类实例化的不同的函数对象其状态也不相同,这是普通函数所无法做到的。而且函数对象是可以在运行时创建。
  • 每个函数对象有自己的类型 :对于普通函数来说,只要签名一致,其类型就是相同的。但是这并不适用于函数对象,因为函数对象的类型是其类的类型。这样,函数对象有自己的类型,这意味着函数对象可以用于模板参数,这对泛型编程有很大提升。
  • 函数对象一般快于普通函数 :因为函数对象一般用于模板参数,模板一般会在编译时会做一些优化。

这里我们看一个可以拥有状态的函数对象,其用于生成连续序列:

可以看到 MeanValue 对象中保存了两个私有变量 num sum 分别记录数量与总和,最后可以通过两者计算出均值。 lambda 表达式也可以利用引用捕捉实现类似功能,但是会有点繁琐。这也算是函数对象独特的优势。

头文件 <functional> 中预定义了一些函数对象,如算术函数对象,比较函数对象,逻辑运算函数对象及按位函数对象,我们可以在需要时使用它们。比如 less<> STL 排序算法中的默认比较函数对象,所以默认的排序结果是升序,但是如果你想降序排列,你可以使用 greater<> 函数对象:

更多有关函数对象的信息大家可以参考 这里

函数适配器

从设计模式来说,函数适配器是一种特殊的函数对象,是将函数对象与其它函数对象,或者特定的值,或者特定的函数相互组合的产物。由于组合特性,函数适配器可以满足特定的需求,头文件 <functional> 定义了几种函数适配器:

  • std::bind(op, args...):将函数对象op的参数绑定到特定的值args
  • std::mem_fn(op):将类的成员函数转化为一个函数对象
  • std::not1(op), std::not2(op):一元取反器和二元取反器

绑定器(binder)

绑定器 std::bind 是最常用的函数适配器,它可以将函数对象的参数绑定至特定的值。对于没有绑定的参数可以使用 std::placeholers::_1,
std::placeholers::_2
等标记。我们从简单的例子开始,比如你想得到一个减去固定树的函数对象:

可以看到, n1 是以默认方式绑定到函数 f 上,故仅是一个副本,不会影响原来的 n1 变量,但是 n2 是以引用绑定的,绑定到 f 的参数与原来的 n2 相互影响, n3 是以const引用绑定的,函数 f 无法修改其值。

绑定器可以用于调用类中的成员函数:

lambda被用来表示一种匿名 函数 ,这种匿名 函数 代表了一种所谓的lambda calculus。以lambda概念为基础的” 函数 式编程“是与命令式编程、面向 对象 编程等并列的一种编程范型。 [capture](parameters)mutable ->return-type{statement} [capture] :捕捉列表,[]是lambda引出符,编译器根据该引出符判断接下来的代码是否是lambda 函数 。捕捉列表用于捕捉父域中的变量以供lambda 函数 使用,[var]表示以值..
1. lambda表达式 语法 c++ 11以后引入了 lambda表达式 lambda表达式 还是很好用的一个东东,谁用谁知道。对比java,java8中也引入了 lambda表达式 ,好评如潮,确实在很多场景中能发挥很大的作用。下面我们就来看看 lambda表达式 的使用。 lambda表达式 的完整声明语法如下 [capture list] (params list) mutable exception-> return type { function body } capture list: 表示捕获的外部变量
问题一:lambda 函数 lambda 函数 (匿名 函数 ): C++ 在C11标准中引入了匿名 函数 ,即没有名字的临时 函数 ,又称之为 lambda表达式 . lambda表达式 实质上是创建一个匿名 函数 / 对象 。 可以在本作用域内自动调用所有变量 完成对应的 函数 要求(而且可以有返回值),可以理解为一种新的传参方式,但是经过测试无法跨作用域。也许对于大牛们是很方便的使用(小白的理解,还望大佬们改正)。 格式...
lambda表达式 lambda表达式 又称为匿名 表达式 ,是C11提出的新语法。[]存储 lambda表达式 要捕获的值,()内的参数为形参,可供外部调用传值。 lambda表达式 可以 直接 调用 // 1 匿名调用 [](string name) cout << "this is anonymous" << endl; cout << "hello " << name << endl;