目录
一、lambda表达式
1.引入
2、lambda表达式语法
二、包装器---function
1.引入
2.包装器介绍
三、bind
一、lambda表达式
1.引入
class Person {
public:
Person(int age,string name)
:_age(age)
,_name(name)
{}
//private://方便后面的举例
int _age;
string _name;
};
int main()
{
vector<Person>v = { {10,"zhangsan"},{40,"wangwu"},{20,"lisi"} };
//当我们要通过姓名/年龄排序时,我们应该怎么办?
//sort(v.begin(),v.end());
return 0;
}
根据我们之前学过的知识,我们可以写两个仿函数,分别对应姓名和年龄的比较,如下
struct comp_age {
bool operator()(const Person& x, const Person& y) {
return x._age < y._age;
}
};
struct comp_name {
bool operator()(const Person& x, const Person& y) {
return x._name < y._name;
}
};
但这种方法太麻烦了,而且一旦排序的标准多起来,给仿函数起什么名字都是个问题,所以出了lambada表达式,如下
int main()
{
vector<Person>v = { {10,"zhangsan"},{40,"wangwu"},{20,"lisi"} };
sort(v.begin(), v.end(), [](const Person& x, const Person& y) {x._age < y._age; });
sort(v.begin(), v.end(), [](const Person& x, const Person& y) {x._name < y._name; });
return 0;
}
可以看出lambda表达式实际上一个匿名函数,跟函数很相似
2、lambda表达式语法
lambda表达式书写格式:
[capture-list] (parameters) mutable -> return-type { statement }
lambda表达式各部分说明
- [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
- (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
- mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
- ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
- {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
注意:
在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情
其实就是看着比较复杂,跟一般的函数比起来,就只是少了一个函数名,多了一个捕捉列表而已,所以只要把捕捉列表的功能能清楚就能很好的掌握lambda表达式
捕获列表说明
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
- [var]:表示值传递方式捕捉变量var
- [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
- [&var]:表示引用传递捕捉变量var
- [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
- [this]:表示值传递方式捕捉当前的this指针
int main() { // 最简单的lambda表达式, 该lambda表达式没有任何意义 []{}; // 省略参数列表和返回值类型,返回值类型由编译器推导为int int a = 3, b = 4; [=]{return a + 3; }; // 省略了返回值类型,无返回值类型 auto fun1 = [&](int c){b = a + c; }; fun1(10) cout<<a<<" "<<b<<endl; // 各部分都很完善的lambda函数,除了b是引用捕捉,其他全是传值方式捕捉 auto fun2 = [=, &b](int c)->int{return b += a+ c; }; cout<<fun2(10)<<endl; //这里提醒一下:lambda表达式只能捕捉在它上面定义的变量,在它之后定义的无法捕捉 // 复制捕捉x int x = 10; auto add_x = [x](int a) mutable { x *= 2; return a + x; }; cout << add_x(10) << endl; return 0; }
注意:
a. 父作用域指包含lambda函数的语句块
b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量
[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
c. 捕捉列表不允许变量重复传递,否则就会导致编译错误。
比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
d. 在块作用域以外的lambda函数捕捉列表必须为空。
e. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
f. lambda表达式之间不能相互赋值,即使看起来类型相同
void (*PF)(); int main() { auto f1 = [] {cout << "hello world" << endl; }; auto f2 = [] {cout << "hello world" << endl; }; f1(); f2(); // 此处先不解释原因,等lambda表达式底层实现原理看完后,大家就清楚了 //f1 = f2; // 编译失败--->提示找不到operator=() // 允许使用一个lambda表达式拷贝构造一个新的副本 auto f3(f2); f3(); // 可以将lambda表达式赋值给相同类型的函数指针 PF = f2; PF(); return 0; }
规则其实并不复杂,就是细节比较多,主要是没咋见识过,用多了就会发现它真的很香
这是上面代码的反汇编调试信息,其实lambda表达式的底层实现就是仿函数,只是类型名很奇怪,但都是调用的operator()这个成员函数
注意:一般lambda表达式的类型名都是class lambda_XXXXX这种样式的,采用的是lambda_uuid的编码方式,不同编译器的命名方式不同,但是我们也能看出它是一个类的函数调用
对比仿函数
我们就能进一步理解捕捉列表,它其实和仿函数的成员变量很相似,一个是自己初始化,一个是捕捉已经存在的
二、包装器---function
1.引入
截止到目前,我们已经学了很3种"函数"---普通函数、仿函数、lambda表达式,它们的类型各不相同,一旦有模板需要传函数,就会导致一个问题,传不同类型的"函数",会产生多个实例,如下template<class F, class T> T useF(F f, T x) { static int count = 0; cout << "count:" << ++count << endl; cout << "count:" << &count << endl; return f(x); } double f(double i) { return i / 2; } struct Functor { double operator()(double d) { return d / 3; } }; int main() { // 函数名 cout << useF(f, 11.11) << endl; // 函数对象 cout << useF(Functor(), 11.11) << endl; // lamber表达式 cout << useF([](double d)->double { return d / 4; }, 11.11) << endl; return 0; }
而这一切都是因为类型不同,所以我们需要将它们的类型进行统一包装,避免这种情况发生,所以出了function包装器
2.包装器介绍
std::function 在头文件 < functional >// 类模板 原型如下template < class T > function ; // undefinedtemplate < class Ret , class ... Args >class function < Ret ( Args ...) > ;模板参数说明:
- Ret: 被调用函数的返回类型
- Args…:被调用函数的形参
function类型的对象能用函数指针,lambda表达式,仿函数赋值,前提是参数及返回值和function类型一样,如下
int main() { // 函数名 function<double(double)> func1 = f; cout << useF(func1, 11.11) << endl; cout << typeid(func1).name() << endl; //函数对象 function<double(double)> func2 = Functor(); cout << useF(func2, 11.11) << endl; cout << typeid(func2).name() << endl; //lamber表达式 function<double(double)> func3 = [](double d)->double { return d / 4; }; cout << useF(func3, 11.11) << endl; cout << typeid(func3).name() << endl; return 0; }
可以看出模板只实例化了一份,有点类似多态,传不同的函数,会有不同的效果,而这都是因为function包装器的类型是统一的
有人可能觉得这个用处好像不是很大,但其实在大型项目中,还是很有必要的,而且它的应用场景远不止于此,我们来看看下面的应用场景
正常来说,我们得写if-else语句或者switch语句一个符号一个符号的匹配
但是现在我们可以用包装器function来简化代码,使得它看起来更加优雅
3.类成员函数的包装
class Plus {
public:
int ADDi(int x, int y)
{
return x, y;
}
static double ADDd(double x, double y)
{
return x + y;
}
private:
int a;
};
int main()
{
//1.注意类的成员函数的地址怎么取,&域名::函数名
//2.非静态成员函数,第一个参数可以是类,也可以是指针
function<int(Plus, int, int)>func1 = &Plus::ADDi;//可以是Plus,也可以是Plus*
function<int(Plus*, int, int)>func2 = &Plus::ADDi;
function<double(double, double)>func3 = &Plus::ADDd;
return 0;
}
三、bind
std::bind函数定义在头文件中,是一个 函数模板 ,它就像一个函数包装器(适配器),接受一个可 调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而 言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M 可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺 序调整等操作。// 原型如下: template <class Fn, class... Args> /* unspecified */ bind (Fn&& fn, Args&&... args); // with return type (2) template <class Ret, class Fn, class... Args> /* unspecified */ bind (Fn&& fn, Args&&... args);
看着概念很复杂,我们来写几个看看,就大致明白了
int test(int x, int y)
{
return x * 2 + y * 3;
}
int main()
{
//placeholders::_xx代表的是funcion中的参数,_1代表第一个,_2代表第二个以此类推
function<int(int)>fun1 = bind(test, 2, placeholders::_1);
cout << fun1(1) << endl;
function<int(int)>fun2 = bind(test, placeholders::_1, 2);
cout << fun2(1) << endl;
function<int(int,int)>fun3 = bind(test, placeholders::_2, placeholders::_1);
cout << fun3(2, 3) << endl;
return 0;
}
这里解释一下,这三个打印结果,传参的对应关系如下
很显然,placeholders::_1,_2,…… 对应的是function中写的参数顺序,bind中第一个参数填函数名(当然仿函数对象,lambda表达式都可以),后面的参数分别对应函数参数的顺序。
function包装器的参数可以少于函数的参数,但是bind中传的参数一般要和原函数参数个数对应,我们可以通过bind来固定一些默认的参数值,或者调换一下参数的顺序,让我们用起函数来更加的"舒服"
成员函数的bind
class Plus {
public:
int ADDi(int x, int y)
{
return x, y;
}
static double ADDd(double x, double y)
{
return x + y;
}
private:
int a = 0;
};
int main()
{
//如果只是单纯想想使用该函数的功能,可以将第一个参数写死,虽然第一个参数是指针,这里也可以传对象,可以看作是特例
function<int(int, int)>func1 = bind(&Plus::ADDi, Plus(), placeholders::_1, placeholders::_2);
cout << func1(1, 2) << endl;
//这种是不行的,右值不能被取地址!!!
//function<int(int, int)>func2 = bind(&Plus::ADDi, &Plus(), placeholders::_1, placeholders::_2);
//cout << func2(1, 2) << endl;
function<double(double, double)>func3 = bind(&Plus::ADDd, placeholders::_1, placeholders::_2);
cout << func3(1.1, 2.3) << endl;
return 0;
}