各位国庆节快乐,祝祖国繁荣昌盛!
常见的语言中都提供Lambda语法糖,比如C#, Python, Golang等。本文将探讨下C 11引入的Lambda语法糖。语法糖
是一种让程序员使用更加便利的一种语法,并不会带来额外的功能,比如Lambda
,没有这种语法糖,其可以用已有的语法等价的实现出相应的功能。
有编程实践经验的同学一定能够快速的理解Lamdba
产生的意义,而缺乏编程经验的同学,跟着我一起来梳理下Lamdba
给我们带来了哪些便利性?
函数指针和对象函数
因为笔者用Lambda
最多的场景是回调函数,先说说回调函数。在编程中回调函数
是一个常见的设计方式, 下图是一个常见的同步调用的回调函数:
调用方
访问被调用方
的实现函数InvokeFunction
被调用方
访问调用方
的回调函数CallbackFunction
上述是一个同步调用的回调方式,是实践中,也有可能是一个异步的回调方式。
一般回调的使用场景可以是被调用方
使用调用方
指定的方法去实现内部的一个逻辑。常见的比如:
被调用模块
使用调用模块
指定的方法完成其功能,比如常见的std::sort
- 比如SDK没有写DebugLog的功能,而是通过回调函数的方式,让调用方实现写DebugLog功能。
- 通知机制:在一些场景下,
被调用方
通过回调函数去通知调用模块
,去进行相应操作。 - …
回调的场景应该不止上述描述的这些,这一章节的重点让我们回归到回调函数
和函数对象
(仿函数
)。
回调函数最常见的C和C 中都使用的函数指针
,我们以std::sort
为例。一个vector
容器中存储了若干的Student
信息,想要将这些学生信息根据年龄进行升序排序,于是可以调用std::sort
,并且使用自定义的函数StudentSortFunction
给sort
作为回调函数来完成排序。
#include <algorithm>
#include <iostream>
#include <vector>
struct Student
{
std::string m_strName;
unsigned int m_uAge;
};
void PrintStudentVector(const std::vector<Student>& vecStudents)
{
for (auto&& student : vecStudents)
{
std::cout << student.m_strName.c_str() << ":" << student.m_uAge << std::endl;
}
std::cout << std::endl;
}
bool StudentSortFunction(const Student& student1, const Student& student2)
{
return student1.m_uAge < student2.m_uAge;
}
int main()
{
std::vector<Student> vecStudents= {
{"xiaoqiang", 15},
{"xiaoming", 13},
{"xiaoke", 13}
};
PrintStudentVector(vecStudents);
std::sort(vecStudents.begin(), vecStudents.end(), StudentSortFunction);
//Print after sort
PrintStudentVector(vecStudents);
return 0;
}
C 中有了函数对象概念,我们同样以上述的例子,实现了一个函数对象StudentSort
,其包含一个重载的函数接口bool operator() (const Student& student1, const Student& student2)
,同样可以实现同样的功能。
#include <algorithm>
#include <iostream>
#include <vector>
struct Student
{
std::string m_strName;
unsigned int m_uAge;
};
void PrintStudentVector(const std::vector<Student>& vecStudents)
{
for (auto&& student : vecStudents)
{
std::cout << student.m_strName.c_str() << ":" << student.m_uAge << std::endl;
}
std::cout << std::endl;
}
class StudentSort
{
public:
bool operator() (const Student& student1, const Student& student2)
{
return student1.m_uAge < student2.m_uAge;
}
};
int main()
{
std::vector<Student> vecStudents= {
{"xiaoqiang", 15},
{"xiaoming", 13},
{"xiaoke", 13}
};
PrintStudentVector(vecStudents);
std::sort(vecStudents.begin(), vecStudents.end(), StudentSort());
//Print after sort
PrintStudentVector(vecStudents);
return 0;
}
当然上述的例子函数指针
和函数对象
似乎没有太多区别。我们注意看回调的方法的入参是由被调用方
给定的并且传入的。但是在一些场景,我们是需要在回调方法
中同样传入被调用方的一些信息。这个时候的回调方法一般的形式是, 会传入一个pCtx
,其存储调用方
所需要传递给回调函数的一些信息。
void CallbackFunction(Contex* pCtx, Parameter par1, Parameter par2.....)
在这种情况下函数指针
和函数对象
就有了区别了,函数指针
是没有成员的,而函数对象
是可有成员函数的,这个时候在C 中,回调的方法一般采用函数对象来实现上述的方式, 比如定义了一个回调函数对象CallbackContext callbackContext
设置给被调用方
,被调用方
使用callbackContext(par1, par2)
即完成了回调方法的调用。
class CallbackContex
{
public:
bool operator() (Parameter par1, Parameter par2) { ; };
private:
Contex* m_pCtx;
};
那么也就是说,每次我们设置给被调用
放都需要定义一个class
,将调用方需要设置给被调用方
的变量给打包到一个叫做Contex
中,这个时候手写一个函数对象,感觉比较繁琐。注意只是繁琐,而不是无法实现。
这个时候使用Lambda
来实现就显的十分的方便快捷了,因为其有一个很棒的功能,叫做捕获变量
。接下来让我们一起来看看本文的主角lambda
吧。
Lambda
Lambda
的表达式如上图所示,其主要构成部分就比普通的函数多了一个捕获列表
,主要由5个部分构成。
- 捕获列表,其可以捕获当前上下文的变量,可以是值捕获或者引用捕获
- 函数参数,不用赘述,和普通函数一样
- specifiers, 可选的,主要说明下
mutable
, 默认情况下值捕获,将无法修改其值(可以想象为其成员函数后面跟了个const
),除非设置为mutable
. - 返回值,如果不写表示返回void
- 函数体, 这部分可以使用你捕获列表里面的变量,也可以使用参数列表里面的变量。
看到这里是不是来演练下第一章节的例子,使用Lambda
如何更简洁的写出一个排序的回调, 是不是比较简单。
std::sort(vecStudents.begin(), vecStudents.end(), [](const Student& student1, const Student& student2) -> bool {
return student1.m_uAge < student2.m_uAge;
});
Lambda
的表达式的结果(注意不是返回值)是一个匿名函数对象,我们一般可以使用 auto
来获取其表达式结果,同样也可以使用。std::function<T>
。
下面我们来举个例子让我们来更加好的理解Lambda
, 尤其是值捕获
和引用捕获
。
#include <iostream>
int main()
{
unsigned int uYear = 2020;
unsigned int uMonth = 9;
std::cout << "uYear: " << uYear
<< " Month: " << uMonth << std::endl << std::endl;
auto lambda = [&uYear, uMonth]() -> bool {
uYear = 2021;
std::cout << "uYear: " << uYear
<< " Month: " << uMonth << std::endl << std::endl;
//error C3491: 'uMonth': a by copy capture cannot be modified in a non-mutable lambda
//uMonth = 10;
return true;
};
lambda();
std::cout << "uYear: " << uYear
<< " Month: " << uMonth << std::endl << std::endl;
return 0;
}
这个例子我们可以看到在Lambda
中使用引用捕获
了uYear
, 值捕获
了uMonth
。那么在Lambda
函数体内:
uYear
是main
函数中的uYear
的引用,对uYear
的重新复制为2021
也会影响到main
中uYear
uMonth
只是main
函数中的uMonth
的值传递,默认情况下不能够直接进行改写,除非将Lambda
指定为mutable
。如果其为mutable
, 在函数体内的修改并不会影响main
中uMonth
的改变。
其实上述的Lamdba
表达式可以用下面的类来表达其含义, 这样的表达易于读者去理解Lambda
在编译器中的实现,也能够更好的掌握Lambda
。
class LambdaClass_XXXXX
{
public:
LambdaClass_XXXXX(unsigned int& uYear, unsigned int uMonth) :m_uYear(uYear), m_uMonth(uMonth) {}
bool operator()() const
{
m_uYear = 2021;
std::cout << "uYear: " << m_uYear
<< " Month: " << m_uMonth << std::endl << std::endl;
return true;
}
private:
unsigned int& m_uYear;
unsigned int m_uMonth;
};
LambdaClass_XXXXX
的命名方式是避免的名字冲突。实际可以查看编译器MSVC命名的方式如下图所示:
如果有很多的参数需要捕获,Lambda
也提供了一些简便的方式:
[&, uMonth]
表示uMonth
采用值捕获
,其他可见的变量均采用`引用捕获[=, &uYear]
表示uYear
采用引用捕获
,其他可见的变量均采用值捕获
那么如果捕获列表的变量名字和函数参数名字相同呢? ,我试了几个不同的编译器,结果不相同,有的报错,有的优先选择函数参数,有的优先选择捕获列表。总之使用者尽量避开名字相同的问题。关于这个在Stackoverflow
上也有所讨论: <<Lambda capture and parameter with same name - who shadows the other? (clang vs gcc)>>, 个人的角度来说更希望是编译阶段直接报错。
通过这一章节的内容,你是否能够举一反三了呢?出一道题目给读者做一做吧。
给读者的问题
为了更好的让读者理解Lambda
的实现,请问以下的程序结果输出是什么呢?先想一个答案,然后不确定的同学用编译器跑了试一试吧。如果答案错误,欢迎和笔者一起讨论哦。
#include <iostream>
int main()
{
int iVal = 100;
auto lambda = [iVal]() mutable {
iVal = 100;
std::cout << iVal << std::endl;
};
lambda();
lambda();
return 0;
}
总结
Lambda
是一种让C
对象函数编写更加便利的语法糖,在使用Lambda
的时候一定要理解其实现原理,尤其是捕获列表
的值捕获
和引用捕获
, 以及要注意其生命周期,以防非法的内存访问导致程序出错。另一点就是文中提到的一个注意点,尽量避免捕获列表的变量名称和函数参数的变量名称相同的情况,因为当前的不同编译器的实现不同,否则掉进坑里了哦。