C++ 中的 lambda 表达式

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



什么是 lambda 表达式

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

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

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

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

#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 表达式定义的匿名函数:

#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 的例子:

#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() :

// 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*);
    // ...
};
// HelloWorldScene.cpp
// ...
bool HelloWorld::touchBeganHandler(Touch*, Event*)
{
    // 弹出一个对话框
    MessageBox("touch began", "Title");
    return false;
}

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

// 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 表达式的形式添加事件监听器

// 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 场景中有四个控件,我们希望为这四个控件编写相同的事件处理。考虑如下代码:

// HelloWorldScene.h
#include "cocos2d.h"

class HelloWorld : public cocos2d::LayerColor
{
public:
    //...

private:
    cocos2d::TextFieldTTF *aTf, *bTf;  // 文本输入框控件
    cocos2d::Label *aLabel, *bLabel;   // 文本标签控件
};
// 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 表达式来优化:

// 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