<audio id="audio" title="10 | lambda：函数式编程带来了什么？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e2/57/e2839e9fd15c657822c170e1d3542957.mp3"></audio>

你好，我是Chrono。

在[第1节课](https://time.geekbang.org/column/article/231454)的时候，我就说到过“函数式编程”，但只是简单提了提，没有展开讲。

作为现代C++里的五种基本编程范式之一，“函数式编程”的作用和地位正在不断上升，而且在其他语言里也非常流行，很有必要再深入研究一下。

掌握了函数式编程，你就又多了一件“趁手的兵器”，可以更好地运用标准库里的容器和算法，写出更灵活、紧凑、优雅的代码。

所以，今天我就和你聊聊函数式编程，看看它给C++带来了什么。

## C++函数的特殊性

说到“函数式编程”，那肯定就要先从函数（function）说起。

C++里的函数概念来源于C，是面向过程编程范式的基本部件。但严格来说，它其实应该叫“子过程”（sub-procedure）、“子例程”（sub-routine），是命令的集合、操作步骤的抽象。

函数的目的是封装执行的细节，简化程序的复杂度，但因为它有入口参数，有返回值，形式上和数学里的函数很像，所以就被称为“函数”。

在语法层面上，C/C++里的函数是比较特别的。虽然有函数类型，但不存在对应类型的变量，不能直接操作，只能用指针去间接操作（即函数指针），这让函数在类型体系里显得有点“格格不入”。

函数在用法上也有一些特殊之处。在C/C++里，所有的函数都是全局的，没有生存周期的概念（static、名字空间的作用很弱，只是简单限制了应用范围，避免名字冲突）。而且函数也都是平级的，不能在函数里再定义函数，也就是**不允许定义嵌套函数、函数套函数**。

```
void my_square(int x)           // 定义一个函数
{
    cout &lt;&lt; x*x &lt;&lt; endl;       // 函数的具体内容
}

auto pfunc = &amp;my_square;       // 只能用指针去操作函数，指针不是函数
(*pfunc)(3);                    // 可以用*访问函数
pfunc(3);                       // 也可以直接调用函数指针


```

所以，在面向过程编程范式里，函数和变量虽然是程序里最关键的两个组成部分，但却因为没有值、没有作用域而不能一致地处理。函数只能是函数，变量只能是变量，彼此之间虽不能说是“势同水火”，但至少是“泾渭分明”。

## 认识lambda

好了，搞清楚了函数，现在再来看看C++11引入的lambda表达式，下面是一个简单的例子：

```
auto func = [](int x)          // 定义一个lambda表达式
{   
    cout &lt;&lt; x*x &lt;&lt; endl;      // lambda表达式的具体内容
};  

func(3);                      // 调用lambda表达式

```

暂时不考虑代码里面的语法细节，单从第一印象上，我们可以看到有一个函数，但更重要的，是这个函数采用了赋值的方式，存入了一个变量。

这就是lambda表达式与普通函数最大、也是最根本的区别。

因为lambda表达式是一个变量，所以，我们就可以“按需分配”，随时随地在调用点“**就地**”定义函数，限制它的作用域和生命周期，实现函数的局部化。

而且，因为lambda表达式和变量一样是“一等公民”，用起来也就更灵活自由，能对它做各种运算，生成新的函数。这就像是数学里的复合函数那样，把多个简单功能的小lambda表达式组合，变成一个复杂的大lambda表达式。

如果你比较熟悉C++98，或者看过一些相关的资料，可能会觉得lambda表达式只不过是函数对象（function object）的一种简化形式，只是一个好用的“语法糖”（syntactic sugar）。

大道理上是没错的，但如果把它简单地等同于函数对象，认为它只是免去了手写函数对象的麻烦，那就实在是有点太“肤浅”了。

lambda表达式为C++带来的变化可以说是革命性的。虽然它表面上只是一个很小的改进，简化了函数的声明/定义，但深层次带来的编程理念的变化，却是非常巨大的。

这和C++当初引入bool、class、template这些特性时有点类似，乍看上去好像只是一点点的语法改变，但后果却如同雪崩，促使人们更多地去思考、探索新的编程方向，而lambda引出的全新思维方式就是“函数式编程”——把写计算机程序看作是数学意义上的求解函数。

C++里的lambda表达式除了可以像普通函数那样被调用，还有一个普通函数所不具备的特殊本领，就是可以**“捕获”外部变量**，在内部的代码里直接操作。

```
int n = 10;                     // 一个外部变量

auto func = [=](int x)          // lambda表达式，用“=”值捕获
{
    cout &lt;&lt; x*n &lt;&lt; endl;        // 直接操作外部变量
};

func(3);                    // 调用lambda表达式

```

看到这里，如果你用过JavaScript，那么一定会有种眼熟的感觉。没错，lambda表达式就是在其他语言中大名鼎鼎的“**闭包**”（closure），这让它真正超越了函数和函数对象。

“闭包”是什么，很难一下子说清楚，我就不详细解释了。说得形象一点，你可以把闭包理解为一个“活的代码块”“活的函数”。它虽然在出现时被定义，但因为保存了定义时捕获的外部变量，就可以跳离定义点，把这段代码“打包”传递到其他地方去执行，而仅凭函数的入口参数是无法做到这一点的。

这就导致函数式编程与命令式编程（即面向过程）在结构上有很大不同，程序流程不再是按步骤执行的“死程序”，而是一个个的“活函数”，像做数学题那样逐步计算、推导出结果，有点像下面的这样：

```
auto a = [](int x)      // a函数执行一个功能
            {...} 
auto b = [](double x)    // b函数执行一个功能
            {...}
auto c = [](string str)  // c函数执行一个功能
            {...}

auto f = [](...)        // f函数执行一个功能
            {...}

return f(a, b, c)            // f调用a/b/c运算得到结果


```

你也可以再对比面向对象来理解。在面向对象编程里，程序是由一个个实体对象组成的，对象通信完成任务。而在函数式编程里，程序是由一个个函数组成的，函数互相嵌套、组合、调用完成任务。

不过，毕竟函数式编程在C++里是一种较新的编程范式，而且面向过程里的函数概念“根深蒂固”，我说了这么多，你可能还是不太能领会它的奥妙，这也很正常。

下面我就来讲讲lambda表达式的使用细节，掌握了以后多用，就能够更好地理解了。

## 使用lambda的注意事项

要学好用好lambda，我觉得就是三个重点：语法形式，变量捕获规则，还有泛型的用法。

**1.lambda的形式**

首先你要知道，C++没有为lambda表达式引入新的关键字，并没有“lambda”这样的词汇，而是用了一个特殊的形式“**[]**”，术语叫“**lambda引出符**”（lambda introducer）。

在lambda引出符后面，就可以像普通函数那样，用圆括号声明入口参数，用花括号定义函数体。

下面的代码展示了我最喜欢的一个lambda表达式（也是最简单的）：

```
auto f1 = [](){};      // 相当于空函数，什么也不做

```

这行语句定义了一个相当于空函数的lambda表达式，三个括号“排排坐”，看起来有种奇特的美感，让人不由得想起那句经典台词：“一家人最要紧的就是整整齐齐。”（不过还是差了个尖括号&lt;&gt;）。

当然了，实际开发中不会有这么简单的lambda表达式，它的函数体里可能会有很多语句，所以一定**要有良好的缩进格式**——特别是有嵌套定义的时候，尽量让人能够一眼就看出lambda表达式的开始和结束，必要的时候可以用注释来强调。

```
auto f2 = []()                 // 定义一个lambda表达式
{
    cout &lt;&lt; &quot;lambda f2&quot; &lt;&lt; endl;

    auto f3 = [](int x)         // 嵌套定义lambda表达式
    {
        return x*x;
    };// lambda f3              // 使用注释显式说明表达式结束

    cout &lt;&lt; f3(10) &lt;&lt; endl;
};  // lambda f2               // 使用注释显式说明表达式结束

```

你可能注意到了，在lambda表达式赋值的时候，我总是使用auto来推导类型。这是因为，在C++里，每个lambda表达式都会有一个独特的类型，而这个类型只有编译器才知道，我们是无法直接写出来的，所以必须用auto。

不过，因为lambda表达式毕竟不是普通的变量，所以C++也鼓励程序员**尽量“匿名”使用lambda表达式**。也就是说，它不必显式赋值给一个有名字的变量，直接声明就能用，免去你费力起名的烦恼。

这样不仅可以让代码更简洁，而且因为“匿名”，lambda表达式调用完后也就不存在了（也有被拷贝保存的可能），这就最小化了它的影响范围，让代码更加安全。

```
vector&lt;int&gt; v = {3, 1, 8, 5, 0};     // 标准容器

cout &lt;&lt; *find_if(begin(v), end(v),   // 标准库里的查找算法
            [](int x)                // 匿名lambda表达式，不需要auto赋值
            {
                return x &gt;= 5;        // 用做算法的谓词判断条件 
            }                        // lambda表达式结束
        )
     &lt;&lt; endl;                        // 语句执行完，lambda表达式就不存在了

```

**2.lambda的变量捕获**

lambda的“捕获”功能需要在“[]”里做文章，由于实际的规则太多太细，记忆、理解的成本高，所以我只说几个要点，帮你快速掌握它们：

- “[=]”表示按值捕获所有外部变量，表达式内部是值的拷贝，并且不能修改；
- “[&amp;]”是按引用捕获所有外部变量，内部以引用的方式使用，可以修改；
- 你也可以在“[]”里明确写出外部变量名，指定按值或者按引用捕获，C++在这里给予了非常大的灵活性。

```
int x = 33;               // 一个外部变量

auto f1 = [=]()           // lambda表达式，用“=”按值捕获
{
    //x += 10;            // x只读，不允许修改
};

auto f2 = [&amp;]()         // lambda表达式，用“&amp;”按引用捕获
{
    x += 10;            // x是引用，可以修改
};

auto f3 = [=, &amp;x]()       // lambda表达式，用“&amp;”按引用捕获x，其他的按值捕获
{
    x += 20;              // x是引用，可以修改
};

```

“捕获”也是使用lambda表达式的一个难点，关键是要理解“**外部变量**”的含义。

我建议，你可以简单地按照其他语言的习惯，称之为“**upvalue**”，也就是在lambda表达式定义之前所有出现的变量，不管它是局部的还是全局的。

这就有一个变量生命周期的问题。

使用“[=]”按值捕获的时候，lambda表达式使用的是变量的独立副本，非常安全。而使用“[&amp;]”的方式捕获引用就存在风险，当lambda表达式在离定义点“很远的地方”被调用的时候，引用的变量可能发生了变化，甚至可能会失效，导致难以预料的后果。

所以，我建议你在使用捕获功能的时候要小心，对于“就地”使用的小lambda表达式，可以用“[&amp;]”来减少代码量，保持整洁；而对于非本地调用、生命周期较长的lambda表达式应慎用“[&amp;]”捕获引用，而且，最好是在“[]”里显式写出变量列表，避免捕获不必要的变量。

```
class DemoLambda final
{
private:
    int x = 0;
public:
    auto print()              // 返回一个lambda表达式供外部使用
    {
        return [this]()      // 显式捕获this指针
        {
            cout &lt;&lt; &quot;member = &quot; &lt;&lt; x &lt;&lt; endl;
        };
    }
};

```

**3.泛型的lambda**

在C++14里，lambda表达式又多了一项新本领，可以实现“**泛型化**”，相当于简化了的模板函数，具体语法还是利用了“多才多艺”的auto：

```
auto f = [](const auto&amp; x)        // 参数使用auto声明，泛型化
{
    return x + x;
};

cout &lt;&lt; f(3) &lt;&lt; endl;             // 参数类型是int
cout &lt;&lt; f(0.618) &lt;&lt; endl;         // 参数类型是double

string str = &quot;matrix&quot;;
cout &lt;&lt; f(str) &lt;&lt; endl;          // 参数类型是string

```

这个新特性在写泛型函数的时候非常方便，摆脱了冗长的模板参数和函数参数列表。如果你愿意的话，可以尝试在今后的代码里都使用lambda来代替普通函数，能够少写很多代码。

## 小结

今天我讲了lambda表达式。它不仅仅是对旧有函数对象的简单升级，而是更高级的“闭包”，给C++带来了新的编程理念：函数式编程范式。

在C语言里，函数是一个“静止”的代码块，只能被动地接受输入然后输出。而lambda的出现则让函数“活”了起来，极大地提升了函数的地位和灵活性。

比照“智能指针”的说法，lambda完全可以称为是“智能函数”，价值体现在就地定义、变量捕获等能力上，它也给C++的算法、并发（线程、协程）等后续发展方向铺平了道路，在后面讲标准库的时候，我们还会多次遇到它。

虽然目前在C++里，纯函数式编程还比较少见，但“轻度”使用lambda表达式也能够改善代码，比如用“map+lambda”的方式来替换难以维护的if/else/switch，可读性要比大量的分支语句好得多。

小结一下今天的要点内容：

1. lambda表达式是一个闭包，能够像函数一样被调用，像变量一样被传递；
1. 可以使用auto自动推导类型存储lambda表达式，但C++鼓励尽量就地匿名使用，缩小作用域；
1. lambda表达式使用“[=]”的方式按值捕获，使用“[&amp;]”的方式按引用捕获，空的“[]”则是无捕获（也就相当于普通函数）；
1. 捕获引用时必须要注意外部变量的生命周期，防止变量失效；
1. C++14里可以使用泛型的lambda表达式，相当于简化的模板函数。

末了我再说一句，和C++里的大多数新特性一样，滥用lambda表达式的话，就会产生一些难以阅读的代码，比如多个函数的嵌套和串联、调用层次过深。这也需要你在实践中慢慢积累经验，找到最适合你自己的使用方式。

## 课下作业

最后是课下作业时间，给你留两个思考题吧：

1. 你对函数式编程有什么样的理解和认识呢？
1. lambda表达式的形式非常简洁，可以在很多地方代替普通函数，那它能不能代替类的成员函数呢？为什么？

欢迎你在留言区写下你的思考和答案，如果觉得今天的内容对你有所帮助，也欢迎分享给你的朋友。我们下节课见。

<img src="https://static001.geekbang.org/resource/image/5a/0d/5ac283e096d87e582fed017597ba4e0d.jpg" alt="">
