在c++ primer中第一次介绍lambda表达式是在容器的泛型算法这一章节中。在这之前我们使用的可调用对象通常是函数和函数指针,这两个可调用对象也是c语言中用的最多的。而c++加入了其它的可调用对象:lambda表达式和重载了函数调用运算符的类。
接下来我们就来简介 c++ 11 中的 lambda表达式。
从软件开发的角度看,以lambda概念为基础的“函数式编程”(Functional Programming)是与命令式编程(命令式编程也就是面向过程编程)(Imperative Programming)、面向对象编程(Object-orientated Programming)等并列的一种编程范型(Programming Paradigm)。
对于C语言中的函数,函数是完成特定任务的独立程序代码单元。语法规则定义了函数的结构和使用方式,有入口参数,有返回值,形式上和数学里的函数很像,所以就被称为“函数”。
函数封装程序执行的细节,让程序更加模块化,从而提高了程序代码的可读性,更方便后期修改、完善。
在语法层面上,C/C++ 里的函数是比较特别的。虽然有函数类型,但不存在对应类型的变量,不能直接操作,只能用指针去间接操作(即函数指针)。
函数在用法上也有一些特殊之处。在 C/C++ 里,所有的函数都是全局的,没有生存周期的概念(static、名字空间的作用很弱,只是简单限制了应用范围,避免名字冲突)。而且函数也都是平级的,不能在函数里再定义函数,也就是不允许定义嵌套函数、函数套函数。
在面向过程编程范式里,函数和变量虽然是程序里最关键的两个组成部分,但却因为没有值、没有作用域而不能一致地处理。函数只能是函数,变量只能是变量。函数和变量在c语言中不是平级的。
lalambda 表达式是一个函数对象,是其他语言中所说的“闭包”(closure),用来表示一种匿名函数。一个lambda 表达式表示一个可调用的代码单元,可以理解为一个未命名的内联函数。
lambda表达式的引入,由于lambda 表达式是一个变量,由于变量可以在任何地点定义,因此也就可以随时随地在调用点“就地”定义lambda表达式,限制它的作用域和生命周期,实现函数的局部化。而且,因为 lambda 表达式和变量一样是“一等公民”,用起来也就更灵活自由,能对它做各种运算,生成新的函数。这就像是数学里的复合函数那样,把多个简单功能的小 lambda 表达式组合,变成一个复杂的大 lambda 表达式。
c语言中,函数和变量不是平级的,函数也没有生存周期,作用域只能用static关键字限制,c++中可以用命名空间限制,但这种限制能力也较弱。
而lambda表达式,将函数当成一个变量,两者平级,因此就可以来限制lambda表达式的作用域和生命周期,实现函数局部化。
lambda函数跟普通函数相比不需要定义函数名,取而代之的多了一对方括号([])。此外,lambda函数还采用了追踪返回类型的方式声明其返回值。其余方面看起来则跟普通函数定义一样。
lambda表达式的形式如下:
[capture list] (parameter list) -> return type {function body}
[capture list]是一个lambda所在函数中定义的局部变量的列表,parameter list、return type和{function body}和普通的函数一样,分别表示函数参数列表,返回值和函数体。但是与普通函数不一样,lambda表达式必须使用尾置返回来指定返回类型。
其中,可以忽略(parameter list)和return type。但必须永远包含[capture list]和{function body}。
如下所示:
#include
int main()
{
//一个最简单的lambda表达式,忽略(parameter list)和return type
//空函数
auto fun1 = []{};
//空函数
auto fun2 = [](){};
//在写lambda表达式时,最好在最后面加个注释表示lambda表达式已结束
//可以看出lambda 表达式的开始和结束
//创建lambda表达式fun3
auto fun3 = []{return 25;};//fun3 lambda
//调用lambda表达式fun3
std::cout << fun3() << std::endl;
return 0;
}
在 lambda 表达式赋值的时候,使用 auto 来推导类型。这是因为,在 C++ 里,每个 lambda 表达式都会有一个全局唯一的独特类型,而这个类型只有编译器才知道,我们是无法直接写出来的,所以必须用 auto。
除去在语法层面上的不同,lambda和仿函数却有着相同的内涵—都可以捕捉一些变量作为初始状态,并接受参数进行运算。而事实上,仿函数是编译器实现lambda的一种方式。在现阶段,通常编译器都会把lambda函数转化为成为一个仿函数对象。因此,在C++11中,lambda可以视为仿函数的一种等价形式了。
#include
int main()
{
auto fun = [](int x, int y)
{
return x+y;
};//fun lambda
std::cout << fun(2, 5) << std::endl;
return 0;
}
借助于C++ Insights网站:https://cppinsights.io/
编译器会将lambda表达式自动转换为仿函数对象,编译器会为此生成个唯一的命名,比如:__lambda_6_16。
#include
int main()
{
class __lambda_6_16
{
public:
inline /*constexpr */ int operator()(int x, int y) const
{
return x + y;
}
using retType_6_16 = int (*)(int, int);
inline constexpr operator retType_6_16 () const noexcept
{
return __invoke;
};
private:
static inline /*constexpr */ int __invoke(int x, int y)
{
return __lambda_6_16{}.operator()(x, y);
}
};
__lambda_6_16 fun = __lambda_6_16{};
return 0;
}
lambda函数与普通函数可见的最大区别之一,就是lambda函数可以通过捕捉列表访问一些上下文中的数据。具体地,捕捉列表描述了上下文中哪些的数据可以被lambda使用,以及使用方式(以值传递的方式或引用传递的方式)。
类似于参数传递,lambda 变量的捕获也可以是值或引用。
默认情况下,捕获的列表的变量都是值捕获,也就是拷贝 local variable 。
ambda 表达式使用“[=]”的方式按值捕获,使用“[&]”的方式按引用捕获,空的“[]”则是无捕获(也就相当于普通函数)。
与传值参数类似,采用值捕获的前提是变量可以拷贝。与参数不同的是,被捕获的变量的值是在lamdba创建时拷贝,而不是调用时拷贝。当使用值捕获时,lambda创建时捕获的变量不会伴随着该变量的修改而改动。
#include
int main()
{
int x = 25;
//创建lambda表达式,采用值捕获的方式捕获局部变量x,拷贝 x 局部变量的值
auto fun = [x]()
{
return x;
};//lambda fun
//局部变量x改变,不会影响lambda fun捕获到x的值
x = 22;
//由于是值捕获,fun()捕获的变量x仍然是25
std::cout << fun() << std::endl;
return 0;
}
lambda表达式引用捕获变量与其他任何类型的引用行为类型,当我们在lambda表达式使用此变量时,实际上使用的是引用所绑定的对象。
#include
int main()
{
int x = 25;
//创建lambda表达式,采用引用捕获的方式捕获局部变量x,引用 x 局部变量的值
auto fun = [&x]()
{
return x;
};//lambda fun
//局部变量x改变,lambda fun捕获到的值已经与x绑定,也发生了改变
x = 22;
//由于是引用捕获,调用时,func 中 x 的值与局部变量绑定,为22
std::cout << fun() << std::endl;
return 0;
}
当我们采用引用方式捕获一个变量时,就必须确保被引用的对象在lambda表达式执行的时候时存在,因为lambad表达式都是捕获的局部变量,局部变量的生存期在函数结束后就不存在了。
c++14之后支持泛型lambda,参数类型用auto声明,相当于自动声明了模板(lambda 表达式的定义过程中是不可以写 template 关键字的),如下所示:
#include
#include
int main()
{
auto fun = [](const auto & x)
{
return x + x;
};
std::cout << fun(3) << std::endl;
std::cout << fun(2.4) << std::endl;
std::cout << fun((std::string)"abcd") << std::endl;
return 0;
}
C++14 里可以使用泛型的 lambda 表达式,相当于简化的模板函数。
注意:lambda 表达式捕获变量时保持简单化,尽量减少捕获的数据量,以及尽可能避免捕获指针和引用。
lambda 表达式是一个局部的函数变量,通常对于实现一个操作我们都会写一个函数,同时为它命名,有时候这个操作过去简单不必要为它去它命名,并且有时候我们只在一个代码段中使用该局部操作,没必要为该操作写一个函数,一个函数的最小作用域都是其所在的cpp文件,而我们只需要该函数的作用域在一个局部的代码段中,这样我们可以就地书写lambda 表达式,就地使用该表达式,可以在一个代码段中重用代码,无需去费力设计函数接口,用一个匿名的函数就可以了,简化了函数的声明 / 定义,让代码更简洁。
lambda 表达式让函数具有了生命周期和明显的作用域,实现了函数的局部化,和变量一样有局部函数的概念了。
lambda 表达式通常仅仅在一个代码段中的一两处定义的简单操作,如果要在多个多个代码段中使用该操作,那么最好用函数,同时lambda 表达式内容往往很简单。
对于小函数而言,lambda 表达式代码的可读性可能更好。
C++ Primer中文版
极客时间:罗剑锋的 C++ 实战笔记
深入理解C++11:C++11新特性解析与应用