参考链接:

引子

想象一下数学中的“函数”。比如我们有一个 $f(x) = 2x + 1$,给定一个 $x$ 值,经过函数我们会得到 $2x+1$。

和我们在编程语言中通常说的函数来比较一下,发现我们平时所说的“函数”,由于庞大繁杂,更像是一个“子过程”。

匿名函数

没有和标识符绑定的函数定义叫做匿名函数(anonymous function),也可以叫做 Lambda 表达式(lambda expression)。匿名函数一般会用作高阶函数的参数,或者在需要返回一个函数的场景下使用。

如果有个函数(若干操作)只需要用一次或者几次,或者说要实现的操作比较轻巧,那么采用匿名函数会比使用具名函数更加简便。

参考链接:

匿名函数抽象出一种运算,比如,想给数组中的每个元素加 1,可以这样写:

  1. #include <vector>
  2. int main() {
  3. std::vector<int> a = {1, 2, 3, 4, 5};
  4. for (auto &i : a) i = i + 1;
  5. }

但是写成下面这样会更清楚地看出,语句是对数组中的每个元素做了一个“加1”的操作;

  1. #include <vector>
  2. #include <algorithm>
  3. int main() {
  4. std::vector<int> a = {1, 2, 3, 4, 5};
  5. std::for_each(a.begin(), a.end(), [](auto & x){ x = x + 1; });
  6. }

参考链接:

在 Python 中也有类似的语法:

  1. l = [1, 2, 3]
  2. add_one = lambda x : x + 1
  3. l = list(map(add_one, l))
  4. print(l) # [2, 3, 4]

可见,我们定义了一个“加 1”的操作,然后将这个操作映射(map)到了列表的每一个元素上。

函数对象

函数对象,有时也被称作仿函数(functor),顾名思义,就是一个使用起来像函数,但其实并不是函数的东西。它是通过重载类的()运算符来实现的这种效果。

  1. #include <iostream>
  2. struct add_n {
  3. int num;
  4. add_n(int _n) : num(_n) {}
  5. int operator()(int val) const { return num + val; }
  6. };
  7. int main() {
  8. add_n add_16(16);
  9. std::cout << add_16(16); // 输出32
  10. }

上面的代码片中重载了 add_n 类的 operator() 方法,也就是 () 运算符,然后声明了一个 add_n 类的实例 add_16,接着调用了实例的 operator() 方法,看起来就像是使用了一个名为 add_16 的函数。

参考链接:

闭包

闭包可以理解成一保存着函数及其运行环境/状态的包。像上文的 add_n(的实例)就可以看成是一个闭包。

使用Lambda表达式

先来看 C++ 中 Lambda 表达式的几种形式,其中captures是表示需要捕获的变量,也就是匿名函数中需要用到的变量,params是需要接收的参数,ret是返回值类型,body则是函数体

原型 说明
[ captures ] ( params ) -> ret { body } Lambda表达式的原型
[ captures ] ( params ) { body } 返回值可以自动推导,所以可以不用指明返回值
[ captures ] { body } 如果函数不要参数,那么参数也可以省略掉

先从比较简单的几个例子说起:

  1. #include <iostream>
  2. int main() {
  3. auto hello = []{ std::cout << "Hello"; }; // 定义
  4. hello(); // 调用,输出"Hello"
  5. auto add = [](int a, int b) -> int { return a + b; }; // 指明返回类型
  6. auto multiply = [](int a, int b) { return a * b; };
  7. std::cout << "2 + 3 = " << add(2, 3);
  8. std::cout << "2 * 3 = " << multiply(2,3);
  9. }

或者,我们还可以写出这样的程序:

  1. // C++ 20
  2. #include <iostream>
  3. #include <string>
  4. #include <format>
  5. #include <functional>
  6. class MakeSentence {
  7. public:
  8. using StrOp = std::function<std::string(std::string const&)>;
  9. static auto I_Ate(int num, std::string const& what, StrOp plural_form) {
  10. std::string tmplt = "I ate {} {}.";
  11. if (num == 0 || num == 1) {
  12. return std::format(tmplt, num, what);
  13. }
  14. else if (num > 1) {
  15. return std::format(tmplt, num, plural_form(what));
  16. }
  17. else return std::string("I ate nothing.");
  18. }
  19. };
  20. int main() {
  21. auto append_s = [](std::string const& str) { return str + "s"; };
  22. auto append_es = [](std::string const& str) { return str + "es"; };
  23. std::cout << MakeSentence::I_Ate(1, "apple", append_s) << "\n";
  24. std::cout << MakeSentence::I_Ate(2, "apple", append_s) << "\n";
  25. std::cout << MakeSentence::I_Ate(1, "potato", append_es) << "\n";
  26. std::cout << MakeSentence::I_Ate(2, "potato", append_es) << "\n";
  27. }

比如使用标准库的排序的时候也可以用到匿名函数:

  1. #include <iostream>
  2. #include <vector>
  3. #include <string>
  4. #include <algorithm>
  5. int main() {
  6. using item_id_pair = std::pair<std::string, int>;
  7. std::vector<item_id_pair> items = {
  8. {"Melon", 5}, {"Apple", 1}, {"Cherry", 3}
  9. };
  10. auto sortByID = [](const item_id_pair & a, const item_id_pair & b){
  11. return a.second < b.second;
  12. };
  13. std::sort(items.begin(), items.end(), sortByID);
  14. for (auto i : items) {
  15. std::cout << "ID: " << i.second << "\t" << i.first << '\n';
  16. }
  17. }

那么[]有什么用呢(不是为了好看),联系之前提到的的“闭包”,其实[]是用来捕获(capture)作用域中的变量的。捕获的变量就会被封装进这个“包”里面。捕获可以是值传递的方式,也可以是引用。

[]中写变量名时,该变量就会被以值传递的方式捕捉进闭包类中,这种情况下无法修改捕捉到的值。如果想要改动值传递捕获的值,需要使用mutable关键字。

  1. #include <iostream>
  2. int main() {
  3. int x = 10;
  4. // auto foo = [x](07e3001a1d24be4959c2d7009a210f33) { x += a; return x; }; // error!
  5. auto foo = [x](07e3001a1d24be4959c2d7009a210f33) mutable {
  6. x += a;
  7. return x;
  8. };
  9. std::cout << foo(2) << '\n'; // 输出 12
  10. std::cout << foo(3) << '\n'; // 输出 15
  11. std::cout << x; // 输出 10
  12. }

在变量名前加 & 则为引用捕获:

  1. #include <iostream>
  2. int main() {
  3. int x = 10;
  4. auto foo = [&x](07e3001a1d24be4959c2d7009a210f33) { x += a; return x; };
  5. std::cout << foo(2) << '\n'; // 输出 12
  6. std::cout << foo(3) << '\n'; // 输出 15
  7. std::cout << x; // 输出 15
  8. }

[]中写=表示默认以值捕获所有变量,写&表示默认以引用捕获所有变量,如果有例外情况写在后面就好,以逗号,分割,如:

  • [=, &x]:默认以值捕获所有变量,但是x是例外,通过引用捕获;
  • [&, x]:默认以引用捕获所有变量,但是x是例外,通过值捕获;

引用捕获不会延长变量生存期,因此有可能出现悬挂引用(Dangling references),比如这样:

  1. auto make_function(int x) {
  2. return [&](07e3001a1d24be4959c2d7009a210f33) { return x + a; };
  3. }
  4. int main() {
  5. auto foo = make_function(5);
  6. foo(3);
  7. }

在调用函数 foo 的时候,因为临时变量 x 已经被销毁,所以会返回奇奇怪怪的结果。


讲到这里,本篇文章就不深入讨论了,未尽事宜请阅读前文给出的链接,或自行查阅资料。

参考链接: