
20220819 lambda
- 理解路线:闭包行为-》三种闭包实现-》函数对象-》lambda
问题
-
lambda的捕捉变量的范围到底是在哪里,全局变量而已?还是局部变量而已?还是某些遵循特定规则的作用域?遇到相同的变量名称lambda怎样进行捕捉?
-
C++Lambda 作用范围-》在Lambda表达式内可以访问当前作用域的变量,这是Lambda表达式的闭包(Closure)行为。-》
闭包
-
百度定义:
闭包是指可以包含自由(未绑定到特定对象)变量的代码块;
这些变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义(局部变量)??。
“闭包” 一词来源于以下两者的结合:
要执行的代码块(由于自由变量被包含在代码块中,这些自由变量以及它们引用的对象没有被释放)
和为自由变量提供绑定的计算环境(作用域)。
-
通俗理解
闭包有很多种定义,
一种说法是,
闭包是带有上下文的函数,
就是有状态的函数,
更直接一些,就是个类。
一个函数, 带上了一个状态, 就变成了闭包了。
- 闭包定义:带上状态的函数
- 理解:带上状态
什么叫 “带上状态” 呢? 意思是这个闭包有属于自己的变量, 这些个变量的值是创建闭包的时候设置的, 并在调用闭包的时候, 可以访问这些变量.
- 内部包含 static 变量的函数, 不是闭包, 因为这个 static 变量不能捆绑. 你不能捆绑不同的 static 变量. 这个在编译的时候已经确定了.
- 闭包的状态捆绑, 必须发生在运行时.
闭包的三种实现
C++ 里使用闭包有3个办法
重载 operator()/函数对象
-
函数对象:
**重载操作符()的类对象。**当用该对象调用此操作符时,其表现形式如同普通函数调用一般,因此取名叫函数对象。
1
2
3
4
5
6
7
8class A
{
public:
int operator() ( int val )
{
return val > 0 ? val : -val;
}
};因为闭包是一个函数+一个状态, 这个状态通过 隐含的 this 指针传入.
所以 闭包必然是一个函数对象.
默认传入的 this 指针提供了访问成员变量的途径.
-
事实上, lambda 和 bind 的原理都是这个.
用法之直接调用
- 例子
1 | class MyFunctor |
当重载()后,像11行一样传入float数值后,
第一步:MyFunctor 会被传入数据进行初始化
第二步:然后就直接执行重载operator()里面的内容
完美实现了(函数+状态)的实现
- 例二
1 |
|
-
例三
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class MyFunctor
{
public:
MyFunctor(int temp): round(temp) {}
int operator()(int temp) {return temp + round; }
private:
int round;
};
void mytest()
{
int round = 2;
MyFunctor f(round);
std::cout << "result: " << f(1) << std::endl; // operator()(int temp)
return;
}
int main()
{
mytest();
system("pause");
return 0;
}
lambda
例子理解
1 |
|
在上面的实例中std::sort
函数第三个参数应该是传递一个排序规则的函数,但是这个实例中直接将排序函数的实现写在应该传递函数的位置,省去了定义排序函数的过程,对于这种不需要复用,且短小的函数,直接传递函数体可以增加代码的可读性。
[ ] ( args )使用
1 | //lambda返回类型相当于使用decltyp根据返回值推断得到;如果lambda不包含返回语句,推断出的返回类型将为void。 |
这里的
[](int x){return x % 3 == 0;}
就是lambda表达式
[ ]用
1 | auto f = [] { |
更简单体现在没有参数传入
这意味着
该lambda表达式创建的时候不需要传入任何的参数
返回类型可以自动推断?
总结:
lambda的类型推断
在lambda表达式完全由一条返回语句组成的时候可以实现
哪怕if-else式的返回语句这样搞就不行
1 | [](double x)->double{int y = x; return x – y;} |
另一个例子
上述例子如果改成下面这样就是错误的
该例子中,表达式函数体内包含不止一个return语句,所以是错误的
我靠我查资料又看到人家可以编译通过唉!!,lambda的类型推断编译器到底是怎么搞的,靠北
auto lambda/有名 lambda
1 | auto mod3 = [](int x){return x % 3 == 0;} // mod3 a name for the lambda |
[ ]传参区别
1 | //[z]---按值访问变量 |
这里的
[&count13](int x){count13 += x % 13 == 0;});
就是lambda表达式
lambda修饰符
这一部分是可以省略的,常见的修饰符有两个,一个是mutable,另一个是exception
mutable
1 | //非mutable |
1 | //含mutable |
分别测试一下
第一个无法进行编译,因为值传入
第二个可以运行,输出结果是1
原因:
从展开结果可以看出,实际上编译器就是把
lambda
表达式转化成为一个类,lambda
表达式捕获的值为该类的数据成员。上例中lambda
表达式被转化为类__lambda_8_12
,其重载了operator()
,由于使用了mutable
修饰,解除了operator()
的const
修饰(默认情况下是const
的)。数据成员为捕获到的a
,并将其实例化为类对象f
,然后调用了两次operator()
,因此a
值的打印也是累加的,即两次结果分别为1
和2
。总 结
lambda
表达式实际上就是一个独有的无名非联合非聚合类,其捕获的数据是它的类成员,该类重载了
operator(),且默认情况下该成员函数是
const,可以使用
mutable关键字来去除
const
https://baijiahao.baidu.com/s?id=1762930363843792781&wfr=spider&for=pc
还可以改为值引用来进行理解
exception
有待补充
捕获列表
- 捕捉列表不允许变量重复传递
- 总表
1.[]
表示不捕获任何变量
1 | auto function = ([]{ |
2.[var]
表示值传递方式捕获变量var
1 | int num = 100; |
创建时拷贝,不是调用时拷贝
3.[=]
表示值传递方式捕获所有父作用域的变量(包括this)
1 | int index = 1; |
4. [&var]表示引用传递捕捉变量var
1 | int num = 100; |
才有引用的方式捕获变量,必须保证该变量在lambda调用时仍然存在
5.[&]表示引用传递方式捕捉所有父作用域的变量(包括this)
1 | int index = 1; |
6.[this]
表示值传递方式捕捉当前的this指针
我感觉这样子就能够实现对类其他内部数据和函数的访问和输出
1 | #include <iostream> |
7.[=, &] 拷贝与引用混合
[=, &a, &b]
表示以引用传递的方式捕捉变量a
和b
,以值传递方式捕捉其它所有变量。
1 | int index = 1; |
输出是 2 1000
[&, a, this]
表示以值传递的方式捕捉变量a
和this
,引用传递方式捕捉其它所有变量。
不过值得注意的是,捕捉列表不允许变量重复传递。下面一些例子就是典型的重复,会导致编译时期的错误。例如:
[=,a]
这里已经以值传递方式捕捉了所有变量,但是重复捕捉a
了,会报错的;[&,&this]
这里&
已经以引用传递方式捕捉了所有变量,再捕捉this
也是一种重复。
参数列表
1 | auto function = [] (int first, int second){ |
基本格式
- 捕获列表。在C++规范中也称为Lambda导入器, 捕获列表总是出现在Lambda函数的开始处。实际上,
[]
是Lambda引出符。编译器根据该引出符判断接下来的代码是否是Lambda函数,捕获列表能够捕捉上下文中的变量以供Lambda函数使用。 - 参数列表。与普通函数的参数列表一致。如果不需要参数传递,则可以连同括号“()”一起省略。
- 可变规格。
mutable
修饰符, 默认情况下Lambda函数总是一个const
函数,mutable
可以取消其常量性。在使用该修饰符时,参数列表不可省略(即使参数为空)。 - 异常说明。用于Lamdba表达式内部函数抛出异常。
- 返回类型。 追踪返回类型形式声明函数的返回类型。我们可以在不需要返回值的时候也可以连同符号”->”一起省略。此外,在返回类型明确的情况下,也可以省略该部分,让编译器对返回类型进行推导。
- lambda函数体。内容与普通函数一样,不过除了可以使用参数之外,还可以使用所有捕获的变量。
可以访问的变量
Lambda表达式的Lambda主体(标准语法中的复合语句)可以包含普通方法或函数的主体可以包含的任何内容。普通函数和Lambda表达式的主体都可以访问以下类型的变量:
- 捕获变量
- 形参变量
- 局部声明的变量
- 类数据成员,当在类内声明
this
并被捕获时 - 具有静态存储持续时间的任何变量,例如全局变量
利用lambda来实现闭包的时候不可以捕捉全局变量哦
注意事项
-
尽量保持lambda变量捕获的简单化
-
尽量避免捕获指针或者引用,可能变量本身会在捕获和被调用的间隔期间改变,甚至被析构清除
底层
- 编译器会把一个Lambda表达式生成一个匿名类的匿名对象,并在类中重载函数调用运算符,实现了一个
operator()
方法。
1 | auto print = []{cout << "Hello World!" << endl; }; |
编译器会把上面这一句翻译为下面的代码:
1 | class print_class |
bind
理解一
chatgpt生成
std::bind
是一个函数模板,可以将一个可调用对象和一些参数绑定在一起,生成一个新的可调用对象。以下是一些简单的 std::bind
示例:
- 绑定函数和参数
1 | #include <functional> |
在上面的例子中,我们将函数 foo
绑定到 f
,并绑定了三个参数 1、2 和 3。在调用 f()
时,输出了绑定参数后的结果。
- 绑定成员函数和对象
1 | c++Copy code |
在上面的例子中,我们将成员函数 bar
绑定到 f
,并绑定了对象 obj
和两个参数 1 和 2。在调用 f()
时,输出了绑定参数后的结果。
- 绑定函数对象和参数
1 | c++Copy code |
在上面的例子中,我们将函数对象 Sum
绑定到 f
,并绑定了三个参数 1、2 和 3。在调用 f()
时,输出了绑定参数后的结果。
理解例子二
这是用lambda写的传入谓词调用形式
用来完成比较string和一个给定的大小
实在不懂看看下面check_size的实现逻辑
这里是bind的传入谓词调用形式
这里是上面bind传入谓词调用形式中的函数
分析可知:check_size需要传入两个参数
但是我们这个find_if只能接受一个一元谓词,
所以我们需要bind形式来进行小修改
结果如下所示
_1是占位符,起到将 find_if后面调用的时候所传入的唯一参数传入 _1所在的位置
更直观的理解
绑定引用参数
- 会有些数据需要以引用形式传入bind,但是自身无法传入引用同时无法又进行拷贝行为的数据类型,如何进行处理?
- 例子:
lambda形式能够很好的完成这个问题
bind如上直接如法炮制可不行
解决办法“采用标准库中的ref()函数
看下面来理解一下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void foo(int& a) {
++a;
}
void foo2(const int& a){
sdt::cout<<"a="<<a<<"\n";
}
void test_function(std::function<void(void)> fun) {
fun();
}
int main() {
int a = 1;
std::cout << "a = " << a << "\n";
test_function(std::bind(foo, a));
std::cout << "a = " << a << "\n";
test_function(std::bind(foo, std::ref(a)));
std::cout << "a = " << a << "\n";
//test_function(std::bind(foo2,std::cref(a)));
//这一句甚至编译不通过,靠北,之前作者是可以的,cref好像是适用于传入的参数是常数的才行....为什么呢
std::cout<<"a =" <<a<<"\n";
return 0;
}
例子三
QString 里面的具体实现
lambda一般应用
Lambda表达式是C++11引入的一种新特性,它可以创建一个匿名函数对象,常用于在函数中定义一些简短的、局部的函数对象,也可以用于实现函数对象的参数化或泛型编程。
以下是一些常见的Lambda表达式应用例子:
- STL算法中的Lambda表达式
STL算法通常需要一个函数对象作为参数,可以使用Lambda表达式来定义这个函数对象。例如,可以使用Lambda表达式来对一个整数数组进行排序:
1 | cCopy code |
上述Lambda表达式定义了一个比较函数,用于对数组中的元素进行排序。
2.C++11线程库中的Lambda表达式
C++11引入了线程库,可以使用Lambda表达式来定义线程的执行代码。例如,可以使用Lambda表达式来创建一个简单的线程:
1 | cCopy code |
上述Lambda表达式定义了一个函数对象,用于在线程中执行代码。
3.STL容器中的Lambda表达式
Lambda表达式可以用于STL容器中的算法、迭代器和函数对象等。例如,可以使用Lambda表达式来查找一个vector中的最大值:
1 | cCopy code |
上述Lambda表达式定义了一个比较函数,用于查找vector中的最大值。
Lambda表达式的应用不限于上述例子,还可以用于函数式编程、回调函数、闭包等。
4.变量捕获
Lambda函数在C++中被用作匿名函数,它能够捕获外部变量,形成闭包。闭包是一个可调用的对象,它能够访问它创建时所在的上下文中的变量。
Lambda函数可以通过捕获列表来捕获变量。捕获列表是指在方括号[]
内部指定的变量列表。捕获列表可以为空,也可以包含以下内容:
- 按值捕获:以值的形式捕获变量,并在lambda表达式内部创建该变量的副本。使用
=
符号表示按值捕获。例如:[a, b, c]
,表示按值捕获变量a
、b
和c
。 - 按引用捕获:以引用的形式捕获变量,并在lambda表达式内部使用该变量。使用
&
符号表示按引用捕获。例如:[&x, &y]
,表示按引用捕获变量x
和y
。 - 按引用捕获但是允许修改:以引用的形式捕获变量,并在lambda表达式内部修改该变量。使用
&
符号并在变量前面添加mutable
关键字表示按引用捕获但是允许修改。例如:[&z]() mutable { z = z + 1; }
,表示按引用捕获变量z
,并在lambda表达式内部对其进行修改。
下面是一个使用闭包的例子,该例子演示了如何使用lambda函数来捕获外部变量,并将其传递给另一个函数:
1 | c++Copy code |
在上述例子中,lambda函数中捕获了外部变量factor
,并将其作为参数传递给std::for_each
函数。std::for_each
函数对numbers
容器中的每个元素都调用了lambda函数,并将factor
乘以每个元素。最后,程序输出了修改后的numbers
容器中的每个元素。
当使用按引用捕获的方式时,可以使用 mutable
关键字允许在 lambda 表达式中修改被捕获的变量。以下是一个示例代码:
1 | c++Copy code |
在上面的例子中,lambda 表达式通过按引用捕获了变量 x
,并使用 mutable
关键字允许修改。在 lambda 表达式内部,输出了 x
的值,并将 x
的值修改为 20。在 lambda 表达式调用结束后,输出了 x
的原始值,可以看到变量 x
的值被成功修改了。