C++ 中的 lambda 表达式

lambda 表达式是函数式编程语言中一个很 cool 的特性,而 C++11 标准加入了对 lambda 表达式的支持。本篇文章对 C++11 中的 lambda 表达式做一个简单的介绍。



什么是 lambda 表达式

说到 lambda expression 就不能不提 lambda calculus,前者是从后者中衍生出的概念,lambda calculus 有着严格的数学定义,与图灵机有着等价的计算能力。这里只介绍编程语言中的 lambda 表达式概念。

在函数式编程语言中,函数是一等公民。有时我们需要一个函数,但又不想要定义一个具有名字的函数,即我们需要一个匿名函数,而一个 lambda 表达式实际上就是通过表达式的方式定义了一个匿名函数。

C++ 中 lambda 表达式的语法规则

先通过一个简单的例子来看看 C++ 中 lambda 表达式的基本语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
using namespace std;
int main()
{
auto foo = []() { cout << "Hello, Lambda!\n"; };
foo(); // call the function
auto foo2 = [](int x)
{
cout << x << endl;
};
foo2(9); // 9
int y = 2;
auto foo3 = [y](int x)
{
cout << x * y << endl;
};
foo3(5); // 10
return 0;
}

[ capture ] ( params ) { body } 是 lambda 表达式的基本写法。

C++ 中 lambda 表达式以 [] 开头,[] 称为 capture specification ,中括号中间的参数 capture 是我们希望捕获的外部引用参数。圆括号中的 params 是匿名函数的参数,而 body 即函数内容,符合 C++ 中一般函数的写法。



怎么用 lambda 表达式

上面的例子中我们用 auto 定义变量保存了 lambda 表达式定义的函数,然后就可以像普通函数一样进行调用。实际上还可以直接调用 lambda 表达式定义的匿名函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <string>
using namespace std;
int main()
{
string my_name("insaneguy");
[](const string& name) { cout << "My name is " + name << endl; } (my_name);
// "My name is insaneguy"
int n = [](int x, int y) { return x + y; } (2, 3);
cout << n << endl; // 5
return 0;
}

值得注意的是,lambda 表达式返回的实际上是一个匿名的函数对象(functor),或者说是一个仿函数实例,而不是直接一个普通函数。因此我们可以将 lambda 表达式作为参数传入 STL 算法,下面举一个 for_each 的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main()
{
vector<int> numbers;
for (int i = 0; i < 10; ++i)
{
numbers.push_back(i);
}
// print even numbers
for_each(numbers.begin(), numbers.end(), [](int n) {
if (n % 2 == 0)
{
cout << n << " ";
}
});
cout << endl;
return 0;
}



为什么要使用 lambda 表达式

前面举的例子都不能很好体现 lambda 表达式的优点。事实上,利用 lambda 表达式创建那些“只用一次”或者比较短小的函数非常方便和高效。

在编写 GUI (图形用户界面) 程序时,回调(callback)函数往往属于“用过就扔”的,即只在程序中与控件绑定一次,之后不会由用户显式地调用。传统的做法是把回调函数写成类的私有成员方法,现在我们可以使用 lambda 表达式来创建回调函数,既可以减少代码量,又能提高程序可读性。

使用 lambda 表达式构造事件回调(callback)

下面举 Cocos2d-x 3.x 中的控件事件监听绑定的例子。

第一种方式:通过回调函数绑定的形式添加事件监听器

HelloWorld 是游戏的主场景层,其中有一个 Label 标签控件,我们希望点击标签弹出一个对话框。

首先要在 HelloWorld 类中定义一个回调方法 HelloWorld::touchBeganHandler() :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// HelloWorldScene.h
#include "cocos2d.h"
class HelloWorld : public cocos2d::LayerColor
{
public:
static cocos2d::Scene* createScene();
virtual bool init();
// 一个处理触摸(onTouchBegan)事件的回调(callback)方法
virtual bool touchBeganHandler(cocos2d::Touch*, cocos2d::Event*);
// ...
};
1
2
3
4
5
6
7
8
// HelloWorldScene.cpp
// ...
bool HelloWorld::touchBeganHandler(Touch*, Event*)
{
// 弹出一个对话框
MessageBox("touch began", "Title");
return false;
}

然后在 HelloWorld::init() 中绑定事件监听器和回调方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// HelloWorldScene.cpp
// ...
bool HelloWorld::init()
{
//...
// 创建一个 Label 控件并添加到当前层中
auto label = Label::create();
label->setString("Hello Cocos2d-x");
label->setPosition(visibleSize / 2);
addChild(label);
// 为 label 创建一个事件监听器,将 label 的 onTouchBegan 事件与
// HelloWorld::touchBeganHandler() 回调方法进行绑定
auto listener = EventListenerTouchOneByOne::create();
// 利用 Cocos2d-x 中的宏绑定事件监听器与回调方法
listener->onTouchBegan =
CC_CALLBACK_2(HelloWorld::touchBeganHandler, this);
// 为 label 控件添加事件监听器
Director::getInstance()->getEventDispatcher()->
addEventListenerWithSceneGraphPriority(listener, label);
//...
return true;
}

第二种方式:使用 C++ lambda 表达式的形式添加事件监听器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// HelloWorldScene.cpp
// ...
bool HelloWorld::init()
{
//...
// 创建一个 Label 并添加到当前层中
auto label = Label::create();
label->setString("Hello Cocos2d-x");
label->setPosition(visibleSize / 2);
addChild(label);
// 创建一个事件监听器,通过 lambda 表达式创建一个匿名回调方法
// 并与 listener 的 onTouchBegan 进行绑定
auto listener = EventListenerTouchOneByOne::create();
listener->onTouchBegan = [](cocos2d::Touch*, cocos2d::Event* )
{
MessageBox("touch began", "Title");
return false;
};
//...
return true;
}

直观上感受,使用 lambda 表达式的版本代码更少,逻辑清晰,代码的可读性高。

进一步来看,上面的例子中我们只为一个 Label 控件绑定了事件回调,该回调函数只“用”了一次,不需要重用,使用 lambda 表达式不需要额外为 HelloWorld 类添加一个成员方法,减少了类的名字空间被“污染”的可能性。

使用 lambda 表达式还可以帮助我们写出符合 DRY (Don’t Repeat Yourself) 的代码。

使用 lambda 表达式编写 DRY 的代码

现在我们的 HelloWorld 场景中有四个控件,我们希望为这四个控件编写相同的事件处理。考虑如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
// HelloWorldScene.h
#include "cocos2d.h"
class HelloWorld : public cocos2d::LayerColor
{
public:
//...
private:
cocos2d::TextFieldTTF *aTf, *bTf; // 文本输入框控件
cocos2d::Label *aLabel, *bLabel; // 文本标签控件
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// HelloWorldScene.cpp
//...
// 为控件添加事件监听器的方法
void HelloWorld::addListeners()
{
auto director = Director::getInstance(); // 获取 Director 类实例
// 使用 lambda 表达式实现 onTouchBegan 事件回调方法
// 暂时未编写事件处理代码
auto handler = [](Touch *t, Event *e) {
return false;
};
// 为输入框 aTf 添加事件监听
auto aTfClickListener = EventListenerTouchOneByOne::create();
aTfClickListener->onTouchBegan = handler;
director->getEventDispatcher()->
addEventListenerWithSceneGraphPriority(aTfClickListener, aTf);
// 为输入框 bTf 添加事件监听
auto bTfClickListener = EventListenerTouchOneByOne::create();
bTfClickListener->onTouchBegan = handler;
director->getEventDispatcher()->
addEventListenerWithSceneGraphPriority(bTfClickListener, bTf);
// 为 aLabel 添加事件监听
auto aLabelListener = EventListenerTouchOneByOne::create();
aLabelListener->onTouchBegan = handler;
director->getEventDispatcher()->
addEventListenerWithSceneGraphPriority(aLabelListener, aLabel);
// 为 bLabel 添加事件监听
auto bLabelListener = EventListenerTouchOneByOne::create();
bLabelListener->onTouchBegan = handler;
director->getEventDispatcher()->
addEventListenerWithSceneGraphPriority(bLabelListener, bLabel);
}

HelloWorld::addListeners() 方法中为不同控件添加事件监听的代码出现了明显的重复现象,我们可以使用 lambda 表达式来优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// HelloWorldScene.cpp
//...
void HelloWorld::addListeners()
{
auto director = Director::getInstance(); // 获取 Director 类实例
// 使用 lambda 表达式实现 onTouchBegan 事件回调函数
// 暂时未编写事件处理代码
auto handler = [=](Touch *t, Event *e) {
return false;
};
// 使用 lambda 表达式为控件添加事件监听
auto addListenerToTarget = [director, handler](Node *target) {
auto listener = EventListenerTouchOneByOne::create();
listener->onTouchBegan = handler;
director->getEventDispatcher()->
addEventListenerWithSceneGraphPriority(listener, target);
};
addListenerToTarget(aTf);
addListenerToTarget(bTf);
addListenerToTarget(aLabel);
addListenerToTarget(bLabel);
}

现在看起来舒服多了~

注意上面的代码中 auto handler = [=](Touch *t, Event *e) {/*...*/}[=] 表示通过拷贝方式捕获所有外部引用变量。关于 lambda 表达式捕获外部变量的语法有如下几种:

  • [] : 不捕获任何外部变量
  • [&] : 通过引用方式捕获所有外部变量
  • [=] : 通过拷贝方式捕获所有外部变量
  • [=, &foo] : 通过引用方式捕获 foo 变量,其他外部变量通过拷贝方式捕获
  • [bar] : 通过拷贝方式捕获 bar 变量
  • [this] : 捕获当前类的 this 指针



参考资料

  1. Lambda Functions in C++11 - the Definitive Guide
  2. Lambda表达式的示例-MSDN