文章目录
- lambda表达式
- lambda表达式的使用规则
- lambda表达式的用法
- lambda表达式的理解
- 函数对象和lambda表达式
- 包装器
- bind
lambda表达式
首先介绍什么是lambda
表达式,在介绍这个情景前,可以回忆一下算法库中的sort
排序:
// lambda表达式
void lambda1()
{int arr[] = { 1,3,6,5,4,2,8,9,10 };for (auto e : arr)cout << e << " ";cout << endl;sort(arr, arr + sizeof(arr) / sizeof(arr[0]));for (auto e : arr)cout << e << " ";cout << endl;
}
这是可以实现排序的结果的,但排序的默认是使用升序排序,这是因为在算法库中最后一个参数给了缺省参数
因此,如果想要实现降序排序,可以自己定义一种排序的方式
// lambda表达式
struct Compare
{bool operator()(int a, int b){return a > b;}
};void lambda1()
{int arr[] = { 1,3,6,5,4,2,8,9,10 };for (auto e : arr)cout << e << " ";cout << endl;sort(arr, arr + sizeof(arr) / sizeof(arr[0]), Compare());for (auto e : arr)cout << e << " ";cout << endl;
}
此时就可以实现一个降序排序
这样的实现是很有意义的,当遇到不能进行默认比较的时候,例如在比较pair
类型参数等,就不可以直接进行比较,需要手动的定义比较的方式,这都是可以理解的
现在的问题是,这样写有一个不方便的地方,就是传入的Compare
参数并不知道是按照什么规则进行排序的,是升序还是降序?这是不确定的
因此C++11
就引入了lambda
表达式来弥补这方面的措施,lambda
表达式最早出现于Python
语言,因此从某种意义来说可以把它当成是一种全新的语言来学习它,那么下面就介绍它的使用规则
lambda表达式的使用规则
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }
对于上面表达式中的各部分写一个说明:
-
[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
-
(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
-
mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
-
{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
注意点:
在lambda
函数定义的过程中,参数列表和返回值的类型都是可以选择的,而捕捉列表和函数体也可以为空,那么空语句可以定义为[]{}表示这个lambda
函数不做任何事
lambda表达式的用法
void lambda2()
{int arr[] = { 1,3,6,5,4,2,8,9,10 };for (auto e : arr)cout << e << " ";cout << endl;// lambda表达式的完全版写法sort(arr, arr + sizeof(arr) / sizeof(arr[0]), [](int x, int y) -> bool {return x < y; });// 缺省方式的写法sort(arr, arr + sizeof(arr) / sizeof(arr[0]), [](int x, int y) {return x < y; });for (auto e : arr)cout << e << " ";cout << endl;
}
上面演示的就是lambda
表达式的一个例子,可以看到的是,整个函数的组成是由捕获列表,参数列表,mutable
选项(这里没写),返回值类型,函数体所组成的,而事实上这也确实就是lambda
表达式的组成,从这个表达式中看就能很明白的看出lambda
表达式的功能是什么了
lambda
表达式还可以用类似于函数的方式来完成:
void lambda3()
{auto fun1 = [](int x, int y){cout << x + y << endl;};auto fun2 = [](int x, int y){return x + y;};fun1(10, 20);cout << fun2(1, 2) << endl;
}
上面的表达式可以看出,fun1
和fun2
接收了这个表达式,接着就可以用函数调用的方式来对lambda
表达式进行执行,而事实上也确实成功的执行了,是有运行结果的
捕捉列表
捕捉列表是lambda
表达式的一个重要组成部分,它表示了lambda
表达式中可以使用哪些数据,以及使用的是传值还是传引用
1. [var]:表示值传递方式捕捉变量var
2. [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
3. [&var]:表示引用传递捕捉变量var
4. [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
5. [this]:表示值传递方式捕捉当前的this指针
lambda表达式的理解
lambda
表达式从上面来看是有返回值的,那么返回值的类型是什么呢?
运行结果如下所示
void lambda4()
{auto fun1 = [](int x, int y){cout << x + y << endl;return 0;};fun1(10, 20);cout << typeid(fun1).name() << endl;
}
30
class `void __cdecl lambda4(void)'::`2'::<lambda_1>
从中看出,lambda
表达式的类型很奇怪,并不是想象中的是一种固定的模式,而是一种类似于随机值的机制,由此说明,lambda
表达式之间是不可以进行相互赋值的,因为它们在底层是完完全全不一样的,不支持operator=
函数对象和lambda表达式
函数对象是什么?lambda
表达式是什么?
函数对象又被叫做仿函数,如同它字面意思一样,可以像函数一样使用对象,简单来说就是前面在sort
中写的Compare
对象,在它里面重载了一个operator()
,这样近似的可以理解成是把一个对象当成一个函数来使用
来举一个例子:
class Rate
{
public:Rate(double rate) : _rate(rate){}double operator()(double money, int year){return money * _rate * year;}private:double _rate;
};void lambda6()
{double rate = 1;Rate r1(rate);r1(1, 2);auto r2 = [=](double monty, int year)->double {return monty * rate * year;};r2(1, 2);
}
从使用的角度来看,函数对象和lambda
表达式是一样的,都是用括号进行调用,函数对象将rate
作为它的成员变量,在定义对象的时候给出初始值,而lambda
表达式又可以通过捕获列表将该变量直接捕获到
而从它们的底层逻辑考虑,在底层看来表达式的处理方式是一样的,都是按照函数对象来处理的,也就是说,如果定义了一个lambda
表达式,在系统的底层会自动生成一个类,在这个类中会重载一个operator()
包装器
什么是包装器?
function
包装器,也叫做适配器,是C++
中的一个类模板
为什么需要包装器?
回忆一下在学习C和C++
的过程中,对于可调用对象的概念来说,可以如何进行调用?可以调用什么呢?
- 函数指针
- 仿函数
- lambda表达式
借助这三个内容,都可以把对象近似当成一个函数来调用,但是这是有弊端的,例如对于函数指针来说,它的使用非常的复杂,不方便使用,对于阅读者来说也很难进行阅读,对于仿函数来说它自身的包装很重,需要构造一个结构体,在里面实现一个函数的重载,而对于lambda
表达式来说就更有弊端了,它本身是一个匿名的内容,想要实现调用也并不容易,因此这三种调用的方式都有一定的弊端,都不太容易进行调用
那么对此可以如何进行针对性的解决呢?C++11
就引入了一个function
包装器,简单来说就是把这些内容进行了一定程度的包装,在调用的时候直接调用
包装器的使用方法
// 包装器
// 定义几种方式用来实现两个数的相加过程
int func1(int x, int y)
{return x + y;
}struct func2
{int operator()(int x, int y){return x + y;}
};class func3
{
public:static int add1(int x, int y){return x + y;}double add2(double x, double y){return x + y;}
};void function1()
{// 把函数指针包装起来function<int(int, int)> fun1 = func1;cout << "函数指针" << fun1(10, 20) << endl;// 把仿函数包装起来function<int(int, int)> fun2 = func2();cout << "仿函数" << fun2(20, 30) << endl;// 把lambda表达式包装起来function<int(int, int)> fun3 = [](int x, int y) {return x + y; };cout << "lambda表达式" << fun3(40, 50) << endl;// 把类的成员函数包装起来function<int(int, int)> fun4 = &func3::add1;cout << "静态成员函数" << fun4(50, 60) << endl;function<double(func3, double, double)> fun5 = &func3::add2;cout << "非静态成员函数" << fun5(func3(), 1.1, 2.2) << endl;
}
上面演示的就是包装器的使用方法,它的好处之一就是,可以把多种调用的方式变成一种来调用,除了非静态成员函数,这个后面和绑定器结合在一起进行讲解
包装器的底层逻辑可以使得模板实例化的成本降低,在实际的开发中还是有意义的
bind
std::bind
函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object)
,生成一个新的可调用对象来“适应”原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn
,通过绑定一些参数,返回一个接收M
个(M
可以大于N
,但这么做没什么意义)参数的新函数。同时,使用std::bind
函数还可以实现参数顺序调整等操作
bind的使用
关于bind
的使用,可以大致有绑定成员函数,参数调换顺序两种使用场景
所以上面的非静态成员变量实际上也是可以进行改造的,可以改造成这样
绑定成员函数
function<double(double, double)> fun5 = bind(&func3::add2, func3(), placeholders::_1, placeholders::_2);cout << "非静态成员函数" << fun5(1.1, 2.2) << endl;
这个函数相当于是把fun5
死绑在func3
的对象中了
参数调换顺序
// bind
int sub(int x, int y)
{return x - y;
}
void testbind()
{function<int(int, int)> fun1 = sub;cout << fun1(10, 5) << endl;function<int(int, int)> fun2 = bind(sub, placeholders::_2, placeholders::_1);cout << fun2(10, 5) << endl;
}