lambda表达式
在C++98中,如果想对一个结构体数组使用sort排序,那么我们就需要自己些仿函数。
比如以下结构体:
struct Goods { string _name; // 名字 double _price; // 价格 int _evaluate; // 评价 Goods(const char* str, double price, int evaluate) :_name(str) , _price(price) , _evaluate(evaluate) {} };
如果我们希望以价格排序,就可以写出如下仿函数:
struct ComparePriceLess { bool operator()(const Goods& gl, const Goods& gr) { return gl._price < gr._price; } }; struct ComparePriceGreater { bool operator()(const Goods& gl, const Goods& gr) { return gl._price > gr._price; } };
随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法,都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在C++11语法中出现了Lambda表达式
lambda
语法如下:
[capture_list] (parameters) mutable -> return_type {statement}
这个语法看起来比较复杂,我先简单讲拆分一下各个部分:
[capture_list]
:捕捉列表(parameters)
:参数列表mutable
:一个关键字
-> return_type
:返回值类型{statement}
: 函数体
比如这是一个完整的lambda
表达式:
auto add = [](int a, int b)mutable -> int { return a + b; };
很明显的看出,以上函数就是传入两个整数,然后返回两数之和。
lambda
表达式有很多种省略情况
muteble
可以省略,改关键字的具体功能后续讲解auto add = [](int a, int b)-> int { return a + b; };
- 函数的返回值
-> return_type
可以省略,lambda
表达式可以自己推导返回类型
auto add = [](int a, int b) { return a + b; };
- 当函数没有参数时,
(parameters)
参数列表可以省略
auto say_hello = [] { cout << "hello world!" << endl; };
以上函数,就已经是一个非常简单的lambda
表达式了。那么lambda
表达式有什么用呢?
lambda
会返回一个仿函数对象
比如auto add = [](int a, int b) { return a + b; };,其实add就是一个仿函数对象了,我们可以直接按照调用函数的方式来调用这个仿函数:add(1, 2);。但是要注意, lambda表达式返回的仿函数对象,其类名是随机的,因此必须使用auto来接受这个仿函数对象。
现在我们再讲讲lambda
表达式最前面的[]
的作用,其名称为捕获列表,可以捕获父作用域中所有变量。
比如这样:
int x = 1; int y = 2; auto add = [x, y] {return x + y; };
以上代码中,[x, y]
就是在捕获父作用域中的两个变量,那么函数体中就可以直接使用这两个变量了。如果直接通过变量名捕获,此时是传值调用,修改函数体内部的变量,不会影响父作用域的变量。
但是通过直接传值捕获的变量,自带const属性,不允许修改,比如以下代码:
int x = 1; int y = 2; auto add = [x, y] { x += 5; y += 5; };
此时代码就会报错,因为x
和y
是通过捕获列表捕获的变量,传入的参数带有const属性,不允许修改。此时就要用到mutable
了,mutable
可以让被捕获的参数可以修改。
auto add = [x, y] mutable { x += 5; y += 5; };
但是这个写法还是错误的,如果使用了mutable
,就算没有通过参数列表传参,()
也不可以省略:
auto add = [x, y] () mutable { x += 5; y += 5; };
我们也可以以传引用的方式来捕获变量,只需要在变量名前加上&
操作符:
int x = 1; int y = 2; auto add = [&x, &y] { x += 5; y += 5; };
此时修改函数内部的x
和y
,就是在修改父作用域的x
和y
了。这里要注意,如果使用了传引用捕获变量,就算没有mutable
也可以修改参数。
另外的,lambda
还提供了一次性捕获所有父作用域变量的语法,只需要在捕获列表中写=
即可:
int x = 1; int y = 2; auto add = [=] { return x + y; };
[=]
就是一次性捕获了所有父作用域变量的过程,我们可以直接在函数体内部使用父作用域的所有变量。
不过[=]
是以传值的形式捕获父作用域所有变量,而[&]
是以传引用的形式捕获父作用域所有变量:
int x = 1; int y = 2; auto add = [&] { x += 5; y += 5; };
此时修改函数内部的x
和y
,就是在修改父作用域的x
和y
了。这里要注意,如果使用了传引用捕获变量,就算没有mutable
也可以修改参数。
另外的,lambda
还提供了一次性捕获所有父作用域变量的语法,只需要在捕获列表中写=
即可:
int x = 1; int y = 2; auto add = [=] { return x + y; };
[=]
就是一次性捕获了所有父作用域变量的过程,我们可以直接在函数体内部使用父作用域的所有变量。
不过[=]
是以传值的形式捕获父作用域所有变量,而[&]
是以传引用的形式捕获父作用域所有变量:
int x = 1; int y = 2; auto add = [&] { x += 5; y += 5; };
另外的,我们还可以把传值和传引用混合使用,让部分参数传参,部分参数传引用。
[x, &y]:以传值的形式捕获x,以传引用的形式捕获y
[=, &x]:以传值的形式捕获父作用域所有变量,以传引用的形式捕获x
[&, x]:以传值的形式捕获x,以传引用的形式捕获父作用域所有变量
接下来我再次汇总一下lambda
的语法:
各个部分:
[capture_list]
:捕捉列表,可以捕获父作用域的任意变量,有传参和传引用两种形式(parameters)
:参数列表,如果没有参数可以省略
mutable
:如果以传参形式捕获参数,不可修改参数,加上该关键字后可以修改-> return_type
:返回值类型,可以省略,lambda
会自动推导{statement}
: 函数体,不可省略
有了lambda
表达式后,我们在需要仿函数的地方,就无需额外写一个仿函数的类,而是直接写一个lambda
表达式,比如最开始的按照价格排序:
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } }; sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) { return g1._price < g2._price; }); sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) { return g1._price > g2._price; });
因为省略了返回值,我们以比函数还简短的方式完成了仿函数的书写。
但是有一个情况,那就是模板参数中的lambda
表达式。
如果我们想要给一个优先级队列priority_queue
传入一个less
仿函数:
priority_queue<int, vector<int>, less<int>> q;
其中less<int>
就是我们的仿函数,但是less<int>
不是仿函数实例化出的对象,而是一个仿函数类型。也就是说,模板参数中需要的不是仿函数对象,而是仿函数类型。但是lambda
表达式整体返回的类型是仿函数对象,因此以下写法是错误的:
priority_queue<int, vector<int>, [](const int& i1, const int& i2) {return i1 - i2; } > q;
我们不能直接把lambda
当作模板参数传入,此时就要使用decltype
来推导原先的类型:
auto intLess = [](const int& i1, const int& i2) {return i1 - i2; }; priority_queue<int, vector<int>, decltype(intLess)> q;
包装器
在寄快递的时候,快递会进行一次包装,这样我们就可以统一的在上面贴上快递信息,随后以统一的形式管理所有快递。包装器也是如此,包装器可以将具有相似属性的东西包装起来成为一个整体。
function
如果一个变量f,可以按照f()
的形式调用函数,那么称f
是一个可调用对象
回顾一下,现在我们有那些可调用对象
:
- 函数指针,函数名(函数名的本质就是函数指针)
- 仿函数实例化出的对象
lambda
表达式
这三者,都可以直接加一对()
进行函数调用。它们都有各自的缺点:
- 函数指针,函数名:类型复杂,不好用
- 仿函数实例化出的对象:哪怕参数返回值都相同,仿函数之间的类型也不同
lambda
表达式:类型是随机的,必须用auto
接收
可以看到,这三者都有类型方面的大问题,我们也没有一种方式可以把所有参数类型和返回值类型相同的函数,统一的管理起来,让它们都变成一个类型?
包装器function
就可以做到该工作,function
被包含在头文件<functional>
中,是一个类模板,模板原型如下:
template <class T> function; template <class Ret, class... Args> class function<Ret(Args...)>;
其语法为:function<返回值(参数列表)>
,只要所有返回值和参数列表相同的可调用对象,经过这一层封装,都会变成相同的类型。
比如我们现在有如下三个函数:
double func(double x) { return x / 2; } struct Functor { double operator()(double x) { return x / 3; } }; int main() { auto lambadaFunc = [](double d) {return d / 4; }; return 0; }
分别是func
函数,Functor
仿函数,以及lambda
表达式lambadaFunc
。它们的返回值都是double
,参数类型也是double
,因此可以经过包装器包装为function<double<double>>
。
如下:
function<double(double)> func1 = func; function<double(double)> func2 = Functor(); function<double(double)> func3 = lambadaFunc;
此时,三者的类型就都是function<double(double)>
了。
有了这一层包装器,在需要统一管理函数时,就很方便了。比如说我现在要搞一个计算器的map
,往map
中输入哪一个操作符,就调用哪一个函数:
map<char, function<int(int, int)>> opFuncMap = { {'+', [](int x, int y) {return x + y; }}, {'-', [](int x, int y) {return x - y; }}, {'*', [](int x, int y) {return x * y; }}, {'/', [](int x, int y) {return x / y; }} };
由于+ - * /的函数都是lambda表达式,四个表达式的类型都是不可知的,map的第二个模板参数就不知道是啥了。不过我们可以通过function进行包装,把所有函数都包装成function<int(int, int)>类型,最后就可以通过map统一管理了。
我们最后就可以这样调用函数:
opFuncMap['+'](1, 2); opFuncMap['-'](1, 2); opFuncMap['*'](1, 2); opFuncMap['/'](1, 2);
function 的常用操作
- 拷贝和赋值:
function
对象支持拷贝构造和赋值操作,可以将一个function
对象赋值给另一个。- 拷贝或赋值
function
对象时,会拷贝/赋值底层的可调用对象。
例如:
function<int(int)> f1 = [](int x) { return x * x; }; function<int(int)> f2 = f1; // f2 现在也是一个 lambda 表达式
调用:
function
对象支持函数调用操作符()
,可以像调用普通函数一样调用function
对象。- 调用
function
对象时,会调用底层的可调用对象。 - 例如:
function<int(int, int)> add = [](int x, int y) { return x + y; }; int result = add(3, 4); // result 为 7
判空:
function
对象支持bool
类型转换,可以用于判断function
对象是否为空(未初始化)。- 例如:
function<int(int)> f; if (!f) { cout << "f is empty" << endl; }
重置:
function
对象提供reset()
成员函数,可以将function
对象重置为未初始化状态。- 例如:
function<int(int)> f = [](int x) { return x * x; }; f.reset(); // f 现在为空
通过这些常用操作,我们可以更灵活地使用 function
包装器,满足各种需求。比如在需要统一管理不同类型的可调用对象时,function
就显得尤为有用。
bind
bind
翻译后为绑定,其可以对参数进行绑定。其主要有两个功能:改变参数顺序
,给指定参数绑定固定值
。
语法:
bind
是一个函数模板,其接收多个参数,第一个参数为可调用对象,后续参数为该可调用对象的参数。这个参数的语法比较特别,C++11后新增一个命名空间域placeholders
,其内部会存储很多变量,这些变量用于函数的传参,变量的名字为_x
表示第x
个参数。
比如以下代码中:
比如以下代码中:
int sub(int a, int b) { return a - b; } int main() { auto f1 = bind(sub, placeholders::_2, placeholders::_1); f1(3, 5); return 0; }
对于bind(sub, placeholders::_2, placeholders::_1);来说,sub这个参数是一个可调用对象。
placeholders::_2表示第二个参数,placeholders::_1表示第一个参数。
比如这个f1最后拿到了这个bind封装的函数,那么f1(3, 5)执行的并不是3 - 5,而是5 - 3。
这是因为我们特地把placeholders::_2写在前面,f1(3, 5)把第二个5传给了placeholders::_2,把第一个3传给了placeholders::_1。
而最后调用sub函数的时候,placeholders::_1会被传给sub的第一个参数,placeholders::_2则会传给sub的第而个参数。这样我们就完成了函数参数顺序的改变。
再比如以下代码:
int sub(int a, int b) { return a - b; } int main() { auto f2 = bind(sub, 3.14, placeholders::_1); f2(10); return 0; }
bind(sub, 3.14, placeholders::_1)第一个参数为可调用对象sub,第二个参数是一个固定值3.14,那么如果通过f2调用该sub函数,参数a都固定为3.14。比如f2(10)就只传了一个参数,再去调用sub时,就完成3.14 - 10的操作。因此我们可以通过sub把某个参数绑定为固定值。
处理函数返回值:
bind
函数还可以用来处理函数的返回值。示例如下:
int add(int x, int y) { return x + y; } int main() { // 绑定 add 函数,并将返回值乘以 2 auto doubleAdd = bind([](int result) { return result * 2; }, add(placeholders::_1, placeholders::_2)); int result = doubleAdd(3, 4); cout << result << endl; // 输出 14 return 0; }
在这个例子中,我们使用 bind 将 add 函数的返回值传递给一个 lambda 表达式,该 lambda 表达式将返回值乘以 2。最终,doubleAdd 函数会返回 add 函数返回值的两倍。
通过这种方式,我们可以对函数的返回值进行各种处理,如格式化、取模等,从而实现更复杂的功能。
总之,bind
不仅可以用于改变参数顺序和绑定参数,还可以用于绑定成员函数以及处理函数返回值,是一个非常强大的工具。