lamada表达式其实就是一个匿名的函数,可以在参数传参的时候构造这个可调用对象
lambda表达式书写格式:
[capture-list] (parameters) mutable -> return-type { statement }
[capture-list]
捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用
(parameters)
参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
mutable
默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)
->returntype
返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
{statement}
函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
使用示例:
int main() {
//最简单的lamada表达式,表示什么都不做
[] {};
//实现一个add lamada表达式
int a = 3, b = 4;
auto add1 = [](int x, int y)->int {return x + y; };
std::cout << add1(a, b) << std::endl;
//通过捕捉列表实现add lamada表达式
auto add2 = [=]{return a + b; };
std::cout << add2() << std::endl;
//实现一个swap lamaba表达式
a = 1, b = 2;
auto swap1 = [](int& x, int& y) {
int tmp = x;
x = y;
y = tmp;
};
swap1(a, b);
//通过捕捉列表实现swap lamada表达式
/*auto swap2 = [&a, &b] {
int tmp = a;
a = b;
b = tmp;
};*/
auto swap2 = [&] {
int tmp = a;
a = b;
b = tmp;
};
swap2();
return 0;
}
通过上述例子可以看出,lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量。
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用
[var]//表示值传递方式捕捉变量var
[=]//表示值传递方式捕获所有父作用域中的变量(包括this)
[&var]//表示引用传递捕捉变量var
[&]//表示引用传递捕捉所有父作用域中的变量(包括this)
注意:
在块作用域以外的lambda函数捕捉列表必须为空
块作用域中的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表达式完全一样
函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可以直接将该变量捕获到
lambda底层是一个仿函数,无论是捕捉列表捕捉还是参数列表
,都是通过函数传参实现的
每个lambda表达式都会生成唯一的uuid,所以仿函数的名称不一样,返回的类型也不一样
实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的
即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。
function包装器也叫作适配器,C++中的function本质是一个类模板,也是一个包装器
上面提到了几种可调用对象,函数指针,仿函数,lambda表达式,这几种对象是完全不同的类型,而且使用起来不是很方便
函数指针类型太复杂,不方便使用和理解
仿函数类型是一个类名,没有指定调用参数和返回值,得去看operator()的实现才能看出来
lambda表达式在语法层,看不到类型,只能在底层看到其类型,基本都是lambda_uuid
而且这几种对象是不同的类型,如果说这几个对象所作的事情完全相同,参数返回值也都相同,这时把对象作为模板参数传递,尽管对象执行的操作都相同,但是对象是不同的类型, 需要实例化出多个类,浪费了资源。
为了解决上面多种可调用对象的类型问题,就引入了function包装器,function包装器其实是对可调用对象的一层封装
,让参数和返回值相同的不同类型的可调用对象封装成一个类型,使用起来更加方便
// 类模板原型如下
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{
int operator() (int a, int b)
{
return a + b;
}
};
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)> func1 = f;
std::cout << func1(1, 2) << std::endl;
//包装函数对象(仿函数)
std::function<int(int, int)> func2 = Functor();
std::cout << func2(1, 2) << std::endl;
//包装lamber表达式
std::function<int(int, int)> func3 = [](const int a, const int b) {return a + b;};
std::cout << func3(1, 2) << std::endl;
//包装类的静态成员函数
std::function<int(int, int)> func4 = Plus::plusi;
std::cout << func4(1, 2) << std::endl;
//包装类的非静态成员函数
std::function<double(Plus, double, double)> func5 = &Plus::plusd;//对于普通成员的包装一定要加上&,需要通过指针进行调用成员函数
std::cout << func5(Plus(), 1.1, 2.2) << std::endl;//传入类对象,通过对象进行调用
return 0;
}
bind是一个函数模板,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表,也就是调整可调用类型参数
class A{
public:
int printf2(int b = 1){
return b + 2;
}
};
int main(){
//把plus绑定成一个值+10
std::function<int(int)> f2 = std::bind([](int a, int b) { return a + b; }, 10, std::placeholders::_1);//表示传入的第一个参数
//绑定后的f2,只需要传入一个参数就可以调用,10也就是第二个参数已经被绑定好了
std::cout << f2(5) << std::endl;
//绑定固定的可调用对象
//在非静态成员函数中需要传入对象,我们就绑定该对象,每次调用时就不用传入了
std::function<int(int)> f3 = std::bind(&A::printf2, A(), std::placeholders::_1);
std::cout << f3(10) << std::endl;
//调整参数顺序
std::function<int(int, int)> f4 = std::bind([](int a, int b) { return a - b; }, std::placeholders::_2, std::placeholders::_1);
std::cout << f4(1, 2) << std::endl;
return 0;
}
bind是对包装器的可调用类型的进一步封装,可以根据自己的需要进行调整参数的数据及位置,绑定类对象能有优化成员函数的包装使用,更加符合使用习惯