20220819 lambda
明昧 Lv7

20220819 lambda

  • 理解路线:闭包行为-》三种闭包实现-》函数对象-》lambda

问题

  • lambda的捕捉变量的范围到底是在哪里,全局变量而已?还是局部变量而已?还是某些遵循特定规则的作用域?遇到相同的变量名称lambda怎样进行捕捉?

  • C++Lambda 作用范围-》在Lambda表达式内可以访问当前作用域的变量,这是Lambda表达式的闭包(Closure)行为。-》

闭包

  • 百度定义

    闭包是指可以包含自由(未绑定到特定对象)变量的代码块;

    这些变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义(局部变量)??

    “闭包” 一词来源于以下两者的结合:

    要执行的代码块(由于自由变量被包含在代码块中,这些自由变量以及它们引用的对象没有被释放)

    和为自由变量提供绑定的计算环境(作用域)。


  • 通俗理解

    闭包有很多种定义,

    一种说法是,

    闭包是带有上下文的函数,

    就是有状态的函数,

    更直接一些,就是个类。

    一个函数, 带上了一个状态, 就变成了闭包了。


  • 闭包定义:带上状态的函数
  • 理解:带上状态

什么叫 “带上状态” 呢? 意思是这个闭包有属于自己的变量, 这些个变量的值是创建闭包的时候设置的, 并在调用闭包的时候, 可以访问这些变量.

  • 内部包含 static 变量的函数, 不是闭包, 因为这个 static 变量不能捆绑. 你不能捆绑不同的 static 变量. 这个在编译的时候已经确定了.
  • 闭包的状态捆绑, 必须发生在运行时.

闭包的三种实现

C++ 里使用闭包有3个办法

重载 operator()/函数对象

  • 函数对象:

    **重载操作符()的类对象。**当用该对象调用此操作符时,其表现形式如同普通函数调用一般,因此取名叫函数对象。

    1
    2
    3
    4
    5
    6
    7
    8
    class A 
    {
    public:
    int operator() ( int val )
    {
    return val > 0 ? val : -val;
    }
    };

    因为闭包是一个函数+一个状态, 这个状态通过 隐含的 this 指针传入.

    所以 闭包必然是一个函数对象.

    默认传入的 this 指针提供了访问成员变量的途径.

  • 事实上, lambda 和 bind 的原理都是这个.


用法之直接调用

  • 例子
1
2
3
4
5
6
7
8
9
10
11
class MyFunctor
{

public:
MyFunctor(float f) : round(f) {}
int operator()(float f) { return f + round; }
private:
float round;
};
float round = 0.5;
MyFunctor f(round);

当重载()后,像11行一样传入float数值后,

第一步:MyFunctor 会被传入数据进行初始化

第二步:然后就直接执行重载operator()里面的内容

完美实现了(函数+状态)的实现

  • 例二
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;
class CAverage
{
public:
double operator()(int a1, int a2, int a3)
{ //重载()运算符
return (double)(a1 + a2 + a3) / 3;
}
};
int main()
{
CAverage average; //能够求三个整数平均数的函数对象
cout << average(3, 2, 3); //等价于 cout << average.operator(3, 2, 3);
return 0;
}

  • 例三

    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
    #define _CRT_SECURE_NO_WARNINGS

    #include <iostream>
    #include <string>
    #include <memory>
    #include <vector>
    #include <map>


    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
2
3
4
5
6
7
8
9
10
#include <algorithm>
#include <cmath>

void abssort(float* x, unsigned n) {
std::sort(x, x + n,
[](float a, float b) {
return (std::abs(a) < std::abs(b));
}
);
}

在上面的实例中std::sort函数第三个参数应该是传递一个排序规则的函数,但是这个实例中直接将排序函数的实现写在应该传递函数的位置,省去了定义排序函数的过程,对于这种不需要复用,且短小的函数,直接传递函数体可以增加代码的可读性。

[ ] ( args )使用

1
2
3
4
5
6
//lambda返回类型相当于使用decltyp根据返回值推断得到;如果lambda不包含返回语句,推断出的返回类型将为void。
[](int x) {return x % 3 == 0;}

//使用整个lambda表达式替换函数指针或伪函数构造函数
count3 = std::count_if(numbers.begin(), numbers.end(),[](int x){return x % 3 == 0;});

这里的

[](int x){return x % 3 == 0;}

就是lambda表达式

[ ]用

1
2
3
auto f = [] {
cout << "Hello" << '\n';
};

更简单体现在没有参数传入

这意味着

该lambda表达式创建的时候不需要传入任何的参数

返回类型可以自动推断?

总结:

lambda的类型推断

在lambda表达式完全由一条返回语句组成的时候可以实现

哪怕if-else式的返回语句这样搞就不行

1
2
3
4
5
6
7
8
[](double x)->double{int y = x; return x – y;} 
// return type is double


//仅当lambda表达式完全由一条返回语句组成时,自动类型推断才管用
//否则,需要使用新增的返回类型后置语法


另一个例子

image-20240625223216673

上述例子如果改成下面这样就是错误的

image-20240625223248104

该例子中,表达式函数体内包含不止一个return语句,所以是错误的

我靠我查资料又看到人家可以编译通过唉!!,lambda的类型推断编译器到底是怎么搞的,靠北

auto lambda/有名 lambda

1
2
3
4
auto mod3 = [](int x){return x % 3 == 0;} // mod3 a name for the lambda
//可以像使用函数一样使用带有名字的lambda函数
bool result = mod3(z); // result is true if z % 3 == 0

[ ]传参区别

1
2
3
4
5
6
7
8
9
10
//[z]---按值访问变量
//[&count]---按引用访问变量
//[&]---按引用访问所有动态变量
//[=]---按值访问所有动态变量
//[&,ted]---按值访问ted,按引用访问其他动态变量
//其他混合方式也允许
int count13 = 0;
std::for_each(numbers.begin(), numbers.end(),
[&count13](int x){count13 += x % 13 == 0;});//此时count13就可以记录可以整除13的x的数量

这里的

[&count13](int x){count13 += x % 13 == 0;});

就是lambda表达式

lambda修饰符

这一部分是可以省略的,常见的修饰符有两个,一个是mutable,另一个是exception

mutable

1
2
3
4
5
6
7
8
9
//非mutable
void test0()
{
int x = 1;
auto test = [x](){ x++; };
test();
cout << x << '\n';

}
1
2
3
4
5
6
7
8
9
//含mutable
void test0()
{
int x = 1;
auto test = [x]()mutable{ x++; };
test();
cout << x << '\n';

}

分别测试一下

第一个无法进行编译,因为值传入

第二个可以运行,输出结果是1


原因:

从展开结果可以看出,实际上编译器就是把lambda表达式转化成为一个类,lambda表达式捕获的值为该类的数据成员。上例中lambda表达式被转化为类__lambda_8_12,其重载了operator(),由于使用了mutable修饰,解除了operator()const修饰(默认情况下是const的)。数据成员为捕获到的a,并将其实例化为类对象f,然后调用了两次operator(),因此a值的打印也是累加的,即两次结果分别为12

总 结

lambda表达式实际上就是一个独有的无名非联合非聚合类,其捕获的数据是它的类成员,该类重载了operator(),且默认情况下该成员函数是const,可以使用mutable关键字来去除const


https://baijiahao.baidu.com/s?id=1762930363843792781&wfr=spider&for=pc

还可以改为值引用来进行理解

exception

有待补充

捕获列表

  • 捕捉列表不允许变量重复传递
  • 总表

image-20240625223305565


1.[]表示不捕获任何变量
1
2
3
4
5
6
7
auto function = ([]{
std::cout << "Hello World!" << std::endl;
}
);

function();

2.[var]表示值传递方式捕获变量var
1
2
3
4
5
6
7
8
int num = 100;
auto function = ([num]{
std::cout << num << std::endl;
}
);

function();

创建时拷贝,不是调用时拷贝

image-20240625223319117

3.[=]表示值传递方式捕获所有父作用域的变量(包括this)
1
2
3
4
5
6
7
8
9
int index = 1;
int num = 100;
auto function = ([=]{
std::cout << "index: "<< index << ", "
<< "num: "<< num << std::endl;
}
);

function();
4. [&var]表示引用传递捕捉变量var
1
2
3
4
5
6
7
8
9
int num = 100;
auto function = ([&num]{
num = 1000;
std::cout << "num: " << num << std::endl;
}
);

function();

才有引用的方式捕获变量,必须保证该变量在lambda调用时仍然存在

image-20240625223332452

5.[&]表示引用传递方式捕捉所有父作用域的变量(包括this)
1
2
3
4
5
6
7
8
9
10
11
12
int index = 1;
int num = 100;
auto function = ([&]{
num = 1000;
index = 2;
std::cout << "index: "<< index << ", "
<< "num: "<< num << std::endl;
}
);

function();

6.[this]表示值传递方式捕捉当前的this指针

我感觉这样子就能够实现对类其他内部数据和函数的访问和输出

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
#include <iostream>
using namespace std;

class Lambda
{
public:
void sayHello() {
std::cout << "Hello" << std::endl;
};

void lambda() {
auto function = [this]{
this->sayHello();
};

function();
}
};

int main()
{
Lambda demo;
demo.lambda();
}

7.[=, &] 拷贝与引用混合
  • [=, &a, &b]表示以引用传递的方式捕捉变量ab,以值传递方式捕捉其它所有变量。
1
2
3
4
5
6
7
8
9
10
11
12
int index = 1;
int num = 100;
auto function = ([=, &index, &num]{
num = 1000;
index = 2;
std::cout << "index: "<< index << ", "
<< "num: "<< num << std::endl;
}
);

function();

输出是 2 1000

  • [&, a, this]表示以值传递的方式捕捉变量athis,引用传递方式捕捉其它所有变量。

不过值得注意的是,捕捉列表不允许变量重复传递。下面一些例子就是典型的重复,会导致编译时期的错误。例如:

  • [=,a]这里已经以值传递方式捕捉了所有变量,但是重复捕捉a了,会报错的;
  • [&,&this]这里&已经以引用传递方式捕捉了所有变量,再捕捉this也是一种重复。

参数列表

1
2
3
4
5
6
auto function = [] (int first, int second){
return first + second;
};

function(100, 200);

基本格式

image-20240625223347833

  1. 捕获列表。在C++规范中也称为Lambda导入器, 捕获列表总是出现在Lambda函数的开始处。实际上,[]是Lambda引出符。编译器根据该引出符判断接下来的代码是否是Lambda函数,捕获列表能够捕捉上下文中的变量以供Lambda函数使用。
  2. 参数列表。与普通函数的参数列表一致。如果不需要参数传递,则可以连同括号“()”一起省略。
  3. 可变规格。mutable修饰符, 默认情况下Lambda函数总是一个const函数,mutable可以取消其常量性。在使用该修饰符时,参数列表不可省略(即使参数为空)。
  4. 异常说明。用于Lamdba表达式内部函数抛出异常。
  5. 返回类型。 追踪返回类型形式声明函数的返回类型。我们可以在不需要返回值的时候也可以连同符号”->”一起省略。此外,在返回类型明确的情况下,也可以省略该部分,让编译器对返回类型进行推导。
  6. lambda函数体。内容与普通函数一样,不过除了可以使用参数之外,还可以使用所有捕获的变量。

可以访问的变量

Lambda表达式的Lambda主体(标准语法中的复合语句)可以包含普通方法或函数的主体可以包含的任何内容。普通函数和Lambda表达式的主体都可以访问以下类型的变量:

  • 捕获变量
  • 形参变量
  • 局部声明的变量
  • 类数据成员,当在类内声明this并被捕获时
  • 具有静态存储持续时间的任何变量,例如全局变量

利用lambda来实现闭包的时候不可以捕捉全局变量哦

注意事项

  • 尽量保持lambda变量捕获的简单化

  • 尽量避免捕获指针或者引用,可能变量本身会在捕获和被调用的间隔期间改变,甚至被析构清除

底层

  • 编译器会把一个Lambda表达式生成一个匿名类的匿名对象,并在类中重载函数调用运算符,实现了一个operator()方法。
1
2
auto print = []{cout << "Hello World!" << endl; };

编译器会把上面这一句翻译为下面的代码:

1
2
3
4
5
6
7
8
9
10
class print_class
{
public:
void operator()(void) const
{
cout << "Hello World!" << endl;
}
};
// 用构造的类创建对象,print此时就是一个函数对象
auto print = print_class();

bind

理解一

chatgpt生成

std::bind 是一个函数模板,可以将一个可调用对象和一些参数绑定在一起,生成一个新的可调用对象。以下是一些简单的 std::bind 示例:

  1. 绑定函数和参数
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <functional>
#include <iostream>

void foo(int a, int b, int c) {
std::cout << "a = " << a << ", b = " << b << ", c = " << c << std::endl;
}

int main() {
auto f = std::bind(foo, 1, 2, 3); // 将 foo 函数绑定到 f
f(); // 调用 f,输出 "a = 1, b = 2, c = 3"

return 0;
}

在上面的例子中,我们将函数 foo 绑定到 f,并绑定了三个参数 1、2 和 3。在调用 f() 时,输出了绑定参数后的结果。

  1. 绑定成员函数和对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
c++Copy code
#include <functional>
#include <iostream>

class Foo {
public:
void bar(int a, int b) {
std::cout << "a = " << a << ", b = " << b << std::endl;
}
};

int main() {
Foo obj;
auto f = std::bind(&Foo::bar, &obj, 1, 2); // 将成员函数 bar 绑定到 f
f(); // 调用 f,输出 "a = 1, b = 2"

return 0;
}

在上面的例子中,我们将成员函数 bar 绑定到 f,并绑定了对象 obj 和两个参数 1 和 2。在调用 f() 时,输出了绑定参数后的结果。

  1. 绑定函数对象和参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
c++Copy code
#include <functional>
#include <iostream>

struct Sum {
int operator()(int a, int b, int c) const {
return a + b + c;
}
};

int main() {
Sum s;
auto f = std::bind(s, 1, 2, 3); // 将函数对象 s 绑定到 f
std::cout << "sum = " << f() << std::endl; // 输出 "sum = 6"

return 0;
}

在上面的例子中,我们将函数对象 Sum 绑定到 f,并绑定了三个参数 1、2 和 3。在调用 f() 时,输出了绑定参数后的结果。

理解例子二

image-20240625223407992

这是用lambda写的传入谓词调用形式

用来完成比较string和一个给定的大小

实在不懂看看下面check_size的实现逻辑


image-20240625223428831

这里是bind的传入谓词调用形式


image-20240625223442496

这里是上面bind传入谓词调用形式中的函数

分析可知:check_size需要传入两个参数

但是我们这个find_if只能接受一个一元谓词,

所以我们需要bind形式来进行小修改

结果如下所示


image-20240625223505959

_1是占位符,起到将 find_if后面调用的时候所传入的唯一参数传入 _1所在的位置


image-20240625223515442

更直观的理解

绑定引用参数

  • 会有些数据需要以引用形式传入bind,但是自身无法传入引用同时无法又进行拷贝行为的数据类型,如何进行处理?
  • 例子:

image-20240625223526505

lambda形式能够很好的完成这个问题


image-20240625223539256

bind如上直接如法炮制可不行


解决办法“采用标准库中的ref()函数

image-20240625223550406

看下面来理解一下:

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
#include <iostream>
#include <functional>

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;
}

https://www.cnblogs.com/yi-mu-xi/p/9896461.html

例子三

1681025417055

QString 里面的具体实现

lambda一般应用

Lambda表达式是C++11引入的一种新特性,它可以创建一个匿名函数对象,常用于在函数中定义一些简短的、局部的函数对象,也可以用于实现函数对象的参数化或泛型编程。

以下是一些常见的Lambda表达式应用例子:

  1. STL算法中的Lambda表达式

STL算法通常需要一个函数对象作为参数,可以使用Lambda表达式来定义这个函数对象。例如,可以使用Lambda表达式来对一个整数数组进行排序:

1
2
3
4
5
cCopy code
int arr[] = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3};
std::sort(std::begin(arr), std::end(arr), [](int a, int b) {
return a < b;
});

上述Lambda表达式定义了一个比较函数,用于对数组中的元素进行排序。

2.C++11线程库中的Lambda表达式

C++11引入了线程库,可以使用Lambda表达式来定义线程的执行代码。例如,可以使用Lambda表达式来创建一个简单的线程:

1
2
3
4
5
cCopy code
std::thread t([]() {
std::cout << "Hello, world!" << std::endl;
});
t.join();

上述Lambda表达式定义了一个函数对象,用于在线程中执行代码。

3.STL容器中的Lambda表达式

Lambda表达式可以用于STL容器中的算法、迭代器和函数对象等。例如,可以使用Lambda表达式来查找一个vector中的最大值:

1
2
3
4
5
6
cCopy code
std::vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3};
auto max_it = std::max_element(v.begin(), v.end(), [](int a, int b) {
return a < b;
});
std::cout << "The maximum value is " << *max_it << std::endl;

上述Lambda表达式定义了一个比较函数,用于查找vector中的最大值。

Lambda表达式的应用不限于上述例子,还可以用于函数式编程、回调函数、闭包等。

4.变量捕获

Lambda函数在C++中被用作匿名函数,它能够捕获外部变量,形成闭包。闭包是一个可调用的对象,它能够访问它创建时所在的上下文中的变量。

Lambda函数可以通过捕获列表来捕获变量。捕获列表是指在方括号[]内部指定的变量列表。捕获列表可以为空,也可以包含以下内容:

  • 按值捕获:以值的形式捕获变量,并在lambda表达式内部创建该变量的副本。使用=符号表示按值捕获。例如:[a, b, c],表示按值捕获变量abc
  • 按引用捕获:以引用的形式捕获变量,并在lambda表达式内部使用该变量。使用&符号表示按引用捕获。例如:[&x, &y],表示按引用捕获变量xy
  • 按引用捕获但是允许修改:以引用的形式捕获变量,并在lambda表达式内部修改该变量。使用&符号并在变量前面添加mutable关键字表示按引用捕获但是允许修改。例如:[&z]() mutable { z = z + 1; },表示按引用捕获变量z,并在lambda表达式内部对其进行修改。

下面是一个使用闭包的例子,该例子演示了如何使用lambda函数来捕获外部变量,并将其传递给另一个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
c++Copy code
#include <iostream>
#include <vector>
#include <algorithm>

int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
int factor = 10;

std::for_each(numbers.begin(), numbers.end(), [factor](int &number) {
number *= factor;
});

std::for_each(numbers.begin(), numbers.end(), [](int number) {
std::cout << number << " ";
});

return 0;
}

在上述例子中,lambda函数中捕获了外部变量factor,并将其作为参数传递给std::for_each函数。std::for_each函数对numbers容器中的每个元素都调用了lambda函数,并将factor乘以每个元素。最后,程序输出了修改后的numbers容器中的每个元素。

当使用按引用捕获的方式时,可以使用 mutable 关键字允许在 lambda 表达式中修改被捕获的变量。以下是一个示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
c++Copy code
#include <iostream>

int main() {
int x = 10;

// lambda 表达式按引用捕获,但允许修改 x 的值
auto lambda = [&x]() mutable {
std::cout << "x = " << x << std::endl;
x = 20;
std::cout << "modified x = " << x << std::endl;
};

lambda(); // 调用 lambda 表达式
std::cout << "original x = " << x << std::endl; // x 的值被修改了

return 0;
}

在上面的例子中,lambda 表达式通过按引用捕获了变量 x,并使用 mutable 关键字允许修改。在 lambda 表达式内部,输出了 x 的值,并将 x 的值修改为 20。在 lambda 表达式调用结束后,输出了 x 的原始值,可以看到变量 x 的值被成功修改了。

 Comments
Comment plugin failed to load
Loading comment plugin
Powered by Hexo & Theme Keep
Unique Visitor Page View