0%

C++ 中的匿名函数

最近会有一些去年 11 月写的的存货博客发布,我眼中匿名函数最重要的一点:对数据的闭包。

函数对象

在类中,可以重载函数调用运算符 (),此时类的对象可以将具有类似函数的行为,我们称这些对象为函数对象 Function Object 或者仿函数 Functor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

class Demo {
public:
int x{12};
void operator()() {
std::cout << this->x << std::endl;
}
};

int main() {
Demo a;
a();
return 0;
}

这个类的对象在执行时,所用的数据封闭在这个类内,我们可以称其为闭包类型。那么有没有一种方法,想调用个东西,但又不想定义函数或类,且能像上面例子一样可以操作数据呢?答案是匿名函数。

匿名函数

1
2
3
4
5
6
7
#include <iostream>

int main() {
auto demo = [] {std::cout << "hello world" << std::endl;};
demo();
return 0;
}

增加参数和返回值类型:

1
2
3
4
5
6
7
8
#include <iostream>

int main() {
auto demo = [](int a, int b) -> int {return a - b;};
int c = demo(1, 2);
std::cout << c << std::endl;
return 0;
}

-> 表示后置返回值类型,当然也可以忽略,由 cpp 自己推断。

那么来解析一下这个匿名函数,每当你定义一个 lambda 表达式后,编译器会自动生成一个匿名类(这个类当然重载了 () 运算符),我们称为闭包类型 closure type。那么在运行时,这个 lambda 表达式就会返回一个匿名的闭包实例,其实一个右值。所以,我们上面的 lambda 表达式的结果就是一个个闭包。闭包的一个强大之处是其可以通过传值或者引用的方式捕捉其封装作用域内的变量,前面的方括号就是用来定义捕捉模式以及变量。当 lambda 捕捉块为空时,表示没有捕捉任何变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>

int main() {
int x{11};
// 如果不是捕获引用想修改值,可以使用 mutable
auto demo = [&x](int a, int b) -> int {
x--;
return a - b + x;
};
int c = demo(1, 2);
std::cout << c << " " << x << std::endl;
return 0;
}

对于复制传值捕捉方式,类中会相应添加对应类型的非静态数据成员。在运行时,会用复制的值初始化这些成员变量,从而生成闭包。这不可避免有一些开销,因此在 leetcode 刷题使用匿名函数排序时,可能会超时。

  • [],不捕获任何外部变量
  • [x, y…],默认以值的形式捕获指定的多个外部变量(用逗号分隔)
  • [this],以值的形式捕获 this 指针
  • [=],以值的形式捕获所有外部变量
  • [&],以引用形式捕获所有外部变量
  • [=, &x],变量x以引用形式捕获,其余变量以传值形式捕获
  • [&, x],变量x以值的形式捕获,其余变量以引用形式捕获

一些用法,对一个 vector 按照另一个 vector 的顺序进行排序:

1
2
3
sort(v1.begin(), v1.end(), [&](int i, int j){
return v2[i] > v2[j];
});

进阶用法

  1. 函数返回匿名函数时,避免引用捕获,会出现悬挂引用问题。
1
2
3
4
// x 是临时变量,会消失,应该用值捕获
std::function<int(int)> add_x(int x) {
return [&](int a) { return x + a; };
}
  1. 类内的匿名函数,使用 this 捕获当前类对象,进而捕获成员

  2. 捕捉不能复制的对象

1
2
3
4
5
6
// 初始化外部没有的变量
auto z = [str = "string"]{ return str; };
// 捕获不能复制的对象
auto myPi = std::make_unique<double>(3.1415);
auto circle_area = [pi = std::move(myPi)](double r) { return *pi * r * r; };
cout << circle_area(1.0) << endl; // 3.1415
  1. 自动推断类型
1
2
3
auto add = [](auto x, auto y) { return x + y; };
int x = add(2, 3); // 5
double y = add(2.5, 3.5); // 6.0

总结

  1. 匿名函数没有名字
  2. 具名函数可以有自己的变量,可以有外部变量的引用,因此这是闭包的
  3. 匿名函数封闭并包围作用域中的变量,因此也是闭包
  4. 闭包可以被理解为一个附带数据的操作

参考

  1. https://www.jianshu.com/p/d686ad9de817
感谢上学期间打赏我的朋友们。赛博乞讨:我,秦始皇,打钱。

欢迎订阅我的文章