参考链接:
引子
想象一下数学中的“函数”。比如我们有一个 $f(x) = 2x + 1$,给定一个 $x$ 值,经过函数我们会得到 $2x+1$。
和我们在编程语言中通常说的函数来比较一下,发现我们平时所说的“函数”,由于庞大繁杂,更像是一个“子过程”。
匿名函数
没有和标识符绑定的函数定义叫做匿名函数(anonymous function),也可以叫做 Lambda 表达式(lambda expression)。匿名函数一般会用作高阶函数的参数,或者在需要返回一个函数的场景下使用。
如果有个函数(若干操作)只需要用一次或者几次,或者说要实现的操作比较轻巧,那么采用匿名函数会比使用具名函数更加简便。
参考链接:
匿名函数抽象出一种运算,比如,想给数组中的每个元素加 1,可以这样写:
#include <vector>int main() {std::vector<int> a = {1, 2, 3, 4, 5};for (auto &i : a) i = i + 1;}
但是写成下面这样会更清楚地看出,语句是对数组中的每个元素做了一个“加1”的操作;
#include <vector>#include <algorithm>int main() {std::vector<int> a = {1, 2, 3, 4, 5};std::for_each(a.begin(), a.end(), [](auto & x){ x = x + 1; });}
参考链接:
在 Python 中也有类似的语法:
l = [1, 2, 3]add_one = lambda x : x + 1l = list(map(add_one, l))print(l) # [2, 3, 4]
可见,我们定义了一个“加 1”的操作,然后将这个操作映射(map)到了列表的每一个元素上。
函数对象
函数对象,有时也被称作仿函数(functor),顾名思义,就是一个使用起来像函数,但其实并不是函数的东西。它是通过重载类的()运算符来实现的这种效果。
#include <iostream>struct add_n {int num;add_n(int _n) : num(_n) {}int operator()(int val) const { return num + val; }};int main() {add_n add_16(16);std::cout << add_16(16); // 输出32}
上面的代码片中重载了 add_n 类的 operator() 方法,也就是 () 运算符,然后声明了一个 add_n 类的实例 add_16,接着调用了实例的 operator() 方法,看起来就像是使用了一个名为 add_16 的函数。
参考链接:
- C++ lambda表达式与函数对象 - 简书 (jianshu.com) (👈这个文章写的挺不错,下面的内容参照了这篇文章)
闭包
闭包可以理解成一保存着函数及其运行环境/状态的包。像上文的 add_n(的实例)就可以看成是一个闭包。
使用Lambda表达式
先来看 C++ 中 Lambda 表达式的几种形式,其中captures是表示需要捕获的变量,也就是匿名函数中需要用到的变量,params是需要接收的参数,ret是返回值类型,body则是函数体
| 原型 | 说明 |
|---|---|
| [ captures ] ( params ) -> ret { body } | Lambda表达式的原型 |
| [ captures ] ( params ) { body } | 返回值可以自动推导,所以可以不用指明返回值 |
| [ captures ] { body } | 如果函数不要参数,那么参数也可以省略掉 |
先从比较简单的几个例子说起:
#include <iostream>int main() {auto hello = []{ std::cout << "Hello"; }; // 定义hello(); // 调用,输出"Hello"auto add = [](int a, int b) -> int { return a + b; }; // 指明返回类型auto multiply = [](int a, int b) { return a * b; };std::cout << "2 + 3 = " << add(2, 3);std::cout << "2 * 3 = " << multiply(2,3);}
或者,我们还可以写出这样的程序:
// C++ 20#include <iostream>#include <string>#include <format>#include <functional>class MakeSentence {public:using StrOp = std::function<std::string(std::string const&)>;static auto I_Ate(int num, std::string const& what, StrOp plural_form) {std::string tmplt = "I ate {} {}.";if (num == 0 || num == 1) {return std::format(tmplt, num, what);}else if (num > 1) {return std::format(tmplt, num, plural_form(what));}else return std::string("I ate nothing.");}};int main() {auto append_s = [](std::string const& str) { return str + "s"; };auto append_es = [](std::string const& str) { return str + "es"; };std::cout << MakeSentence::I_Ate(1, "apple", append_s) << "\n";std::cout << MakeSentence::I_Ate(2, "apple", append_s) << "\n";std::cout << MakeSentence::I_Ate(1, "potato", append_es) << "\n";std::cout << MakeSentence::I_Ate(2, "potato", append_es) << "\n";}
比如使用标准库的排序的时候也可以用到匿名函数:
#include <iostream>#include <vector>#include <string>#include <algorithm>int main() {using item_id_pair = std::pair<std::string, int>;std::vector<item_id_pair> items = {{"Melon", 5}, {"Apple", 1}, {"Cherry", 3}};auto sortByID = [](const item_id_pair & a, const item_id_pair & b){return a.second < b.second;};std::sort(items.begin(), items.end(), sortByID);for (auto i : items) {std::cout << "ID: " << i.second << "\t" << i.first << '\n';}}
那么[]有什么用呢(不是为了好看),联系之前提到的的“闭包”,其实[]是用来捕获(capture)作用域中的变量的。捕获的变量就会被封装进这个“包”里面。捕获可以是值传递的方式,也可以是引用。
在[]中写变量名时,该变量就会被以值传递的方式捕捉进闭包类中,这种情况下无法修改捕捉到的值。如果想要改动值传递捕获的值,需要使用mutable关键字。
#include <iostream>int main() {int x = 10;// auto foo = [x](07e3001a1d24be4959c2d7009a210f33) { x += a; return x; }; // error!auto foo = [x](07e3001a1d24be4959c2d7009a210f33) mutable {x += a;return x;};std::cout << foo(2) << '\n'; // 输出 12std::cout << foo(3) << '\n'; // 输出 15std::cout << x; // 输出 10}
在变量名前加 & 则为引用捕获:
#include <iostream>int main() {int x = 10;auto foo = [&x](07e3001a1d24be4959c2d7009a210f33) { x += a; return x; };std::cout << foo(2) << '\n'; // 输出 12std::cout << foo(3) << '\n'; // 输出 15std::cout << x; // 输出 15}
在[]中写=表示默认以值捕获所有变量,写&表示默认以引用捕获所有变量,如果有例外情况写在后面就好,以逗号,分割,如:
[=, &x]:默认以值捕获所有变量,但是x是例外,通过引用捕获;[&, x]:默认以引用捕获所有变量,但是x是例外,通过值捕获;
引用捕获不会延长变量生存期,因此有可能出现悬挂引用(Dangling references),比如这样:
auto make_function(int x) {return [&](07e3001a1d24be4959c2d7009a210f33) { return x + a; };}int main() {auto foo = make_function(5);foo(3);}
在调用函数 foo 的时候,因为临时变量 x 已经被销毁,所以会返回奇奇怪怪的结果。
讲到这里,本篇文章就不深入讨论了,未尽事宜请阅读前文给出的链接,或自行查阅资料。
参考链接:
