1 lambda表达式
1.1 引例
在C++98中,如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法:
#include <algorithm>
#include <functional>
int main()
{
int array[] = { 4,1,8,5,3,7,0,9,2,6 };
// 默认按照小于比较,排出来结果是升序
std::sort(array, array + sizeof(array) / sizeof(array[0]));
// 如果需要降序,需要改变元素的比较规则
std::sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>());
return 0;
}
如果待排序元素为自定义类型,需要用户定义排序时的比较规则:
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;
}
};
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
3 }, { "菠萝", 1.5, 4 } };
sort(v.begin(), v.end(), ComparePriceLess());
sort(v.begin(), v.end(), ComparePriceGreater());
return 0;
}
如果仿函数命名比较规范的话,像上面的命名方式的话那还好,如果遇到了像cmp1 cmp2 cmp3…这种命名方式而且还没有注释的话可以让人烦死,自己还得去找对应的源码实现,而如果在一个工程中有很多代码,找的代价也会比较大,所以C++11便新推出了一个语法就是lambda表达式。
1.2 lambda表达式的基本语法
lambda表达式各部分说明:
- : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
- :参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略。
- :默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
- :返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
- :函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
注意:
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
- [var]:表示值传递方式捕捉变量var
- [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
- [&var]:表示引用传递捕捉变量var
- [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
- [this]:表示值传递方式捕捉当前的this指针
我们可以来实现一个简单的add来验证一下:
int main()
{
int x, y;
cin >> x >> y;
auto add = [=]()
{
return x + y;
};
cout << add() << endl;
return 0;
}
lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量。像上面的add你甚至还可以这样写:cout<< [=](){return x + y;}()<< endl;
我们可以来看看mutable的应用场景,比如下面的代码:
int main()
{
int x = 10,y = 20;
auto swapInt = [=] {int tmp = x; x = y; y = tmp; };
swapInt();
return 0;
}
当我们编译时会直接报错的:
为什么呢?因为我们是用值捕捉的方式捕捉到的变量,而捕捉到的变量是一份拷贝,并且默认是不让你你修改的(可以理解为增加了const属性),所以当你修改变量是会直接报错的,那假如我们想让其修改呢?我们就可以用(意思是易变的):
注意:
- 父作用域指包含lambda函数的语句块。
- 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量。
- 捕捉列表不允许变量重复传递,否则就会导致编译错误。比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复。
- 在块作用域以外的lambda函数捕捉列表必须为空。
- 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
- lambda表达式之间不能相互赋值,即使看起来类型相同。
前面的注意事项都很好理解,最后一个注意点我们可以验证下:
void (*PF)();
int main()
{
auto f1 = []{cout << "hello world" << endl; };
auto f2 = []{cout << "hello world" << endl; };
//f1 = f2; // 编译失败--->提示找不到operator=()
// 允许使用一个lambda表达式拷贝构造一个新的副本
auto f3(f2);
f3();
// 可以将lambda表达式赋值给相同类型的函数指针
PF = f2;
PF();
return 0;
}
注意事项代码中都有注释。
至于为啥不允许赋值,我们后面讲解lambda表达式的原理时会给出解释。
1.3 lambda表达式的底层原理
函数对象,又称为仿函数,即可以像函数一样使用的对象,就是在类中重载了operator()运算符的类对象。
我们写一段代码来验证一下:
class Rate
{
public:
Rate(double rate) : _rate(rate)
{}
double operator()(double money, int year)
{
return money * _rate * year;
}
private:
double _rate;
};
int main()
{
// 函数对象
double rate = 0.49;
Rate r1(rate);
r1(10000, 2);
// lamber
auto r2 = [=](double monty, int year)->double { return monty * rate * year;};
r2(10000, 2);
return 0;
}
从汇编的角度来看,我们不难发现lambda表达式在底层也是调用了operator
来实现,那为什么lambda表达式不能够相互赋值呢?其本质是因为lambda表达式在底层的命名是采用uuid
的方式生成唯一的类名,所以不同类型的对象自然不可以赋值了。
实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如
果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。
那考考大家:lambda对象的大小是多少字节呢❓
答案其实已经显而易见了,由于lambda表达式的底层是用仿函数实现的,而仿函数是一个没有内置成员变量的类(空类),大小就是1字节喽,你回答对了吗?
2 包装器
使用包装器前我们要引入头文件#include <functional>
类模板的原型如下:
// 类模板原型如下
template <class T> function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
那包装器我们日常是如何使用的呢?
// 使用方法如下:
#include <functional>
int f(int a, int b)
{
return a + b;
}
struct Functor
{
public:
int operator() (int a, int b)
{
return a + b;
}
};
int main()
{
// 函数名(函数指针)
std::function<int(int, int)> func1 = f;
cout << func1(1, 2) << endl;
// 函数对象
std::function<int(int, int)> func2 = Functor();
cout << func2(1, 2) << endl;
// lamber表达式
std::function<int(int, int)> func3 = [](const int a, const int b)
{return a + b; };
cout << func3(1, 2) << endl;
return 0;
}
我们可以用包装器来接受 函数指针 仿函数 lambda
,这样我们就可以用统一的类型来接受不同的参数,达到只实例化一份的目的。
但是在调用类中非静态成员函数(不包括仿函数)时要额外注意function的语法格式:
比如下面:
class Plus
{
public:
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return a + b;
}
};
int main()
{
// 类的成员函数
std::function<int(int, int)> func4 = &Plus::plusi;
cout << func4(1, 2) << endl;
std::function<double(Plus, double, double)> func5 = &Plus::plusd;
cout << func5(Plus(), 1.1, 2.2) << endl;
return 0;
}
我们知道静态成员函数是不包括this
指针的,所以用之前的语法是没有问题的,但是由于成员函数有this指针,所以我们就要(我们一般喜欢给匿名对象来调用),通过参数对象来调用里面的成员函数。并且在指定类域是要加上&
,这时语法的硬性规定。
但是大家注意下面这种调用方式:
我们也可以用对象指针来调用,但是这时候就不能够用匿名对象了,因为匿名对象是右值,是不能够&
的,但是一般情况下我们不会选择这种方式。
3 bind
一般来说,我们使用bind有下面这两种情况:
- 1️⃣调换参数顺序
- 2️⃣改变参数个数
其中调换参数顺序其实一般很少用到,而改变参数个数很有意思,我们接下来一个一个来看:
比如下面这段程序:
void Print(int x, int y)
{
cout << x << ":" << y << endl;
}
假设我们不改变Print函数的实现,而打印结果时交换参数顺序,我们可以怎么做?
我们可以用bind
来处理:
int main()
{
int x = 10, y = 20;
Print(x, y);
auto RPrint = bind(Print, placeholders::_2, placeholders::_1);
RPrint(x, y);
return 0;
}
这里面的_1 _2
是什么鬼呀?这其实是封装在placeholders
命名空间中的一个占位符,正如我们直接理解的那样, _1 _2 ……分别代表着第一个参数,第二个参数……,我们想要交换哪些参数的位置可以直接通过交换占位符的顺序即可。
交换参数顺序的用法其实比较鸡肋,我们平时一般也不怎么用到,但是改变参数个数的场景我觉得还是比较有意思的,我们接下来看看这种情况:
void mul(double x, double y)
{
cout<< x * y<<endl;
}
struct fun
{
fun(double rate)
:_rate(rate)
{}
void mulR(double x, double y)
{
cout << x * y * _rate << endl;
}
double _rate;
};
int main()
{
int x = 10, y = 20;
function<void(double, double)> f1 = mul;
function<void(double, double)> f2 = [=](double x,double y) {cout<< x * y<<endl; };
return 0;
}
当我们要求使用跟上面参数一样的格式来接受fun中的mulR时我们直接写是会直接报错的,在上面我们讲解function时已经详细解释了原理,这里就不在多说了。那我们可以通过bind来处理:
function<void(double, double)> f3 = bind(&fun::mulR,f, placeholders::_1, placeholders::_2);
我们可以通过上面的方式来绑定处理,将第一个参数绑定写死,然后我们就可以只用两个参数的包装器来接受了,是不是很妙。当然,我们不仅可以绑死第一个参数,第二个三个n个参数我们都可以通过bind来绑死,值得注意的小细节是不论我们绑死的是第几个参数,我们其他。