lambda表达式
当我们在写代码的时候如果经常需要用到一个公用的功能,为了方便维护,通常就会通过重载类的operator ()来将其写成仿函数(具有函数特性的类)来使用。
从使用的角度来讲,仿函数虽然好用,但是如果需要根据多种不同的情况设计多个函数的时候,这时候使用仿函数无疑是一种折磨,不仅仅代码量增加,而且还有大量的代码冗余。
例如下面这种情况
在我们使用购物网站的时候,通常都可以选择以不同的属性例如名字、价格、好评度、销量等来为商品进行排序
struct Goods
{
string _name; // 名字
double _price; // 价格
int _num; // 数量
// ...
};
/*
如果我们要使用sort来对货物进行排序,就必须要提供给他一个比较的规则,
也就是用仿函数来实现一个大小判断,但是我们并不知道他要按照哪种方式进行比较,
也不知道他是按照>、<、>=、<=等方式进行排序,在属性很多的情况下,代码就会出现下面这样的大量冗余
*/
struct ComparePriceGreater
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price > gr._price;
}
};
struct CompareNumGreater
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._num > gr._num;
}
};
struct CompareNameGreater
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._name > gr._name;
}
};
//..................等等等等
这样的代码不仅有上面的缺点,他的可读性也非常的差,简单的逻辑还好,倘若复杂的话接手代码的人一定心态爆炸
所以C++11引入了lambda表达式来解决这个问题
同样的代码,在引入了lambda表达式后代码的可读性就大大的提升了。
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& g1, const Goods& g2)->bool{return g1._price > g2._price; });
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& g1, const Goods& g2)->bool{return g1._price < g2._price; });
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& g1, const Goods& g2)->bool{return g1._num > g2._num; });
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& g1, const Goods& g2)->bool{return g1._num < g2._num; });
lambda表达式的语法
lambda表达式书写格式:
[capture-list] (parameters) mutable -> returntype { statement }
lambda表达式各部分说明
[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来
的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用,捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
捕获列表:
- [var]:表示值传递方式捕捉变量var
- [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
- [&var]:表示引用传递捕捉变量var
- [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
- [this]:表示值传递方式捕捉当前的this指针
注意事项:
- a. 父作用域指包含lambda函数的语句块
- b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。 用方式捕捉其他变量 c. 捕捉列表不允许变量重复传递,否则就会导致编 译错误。 比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
- d. 在块作用域以外的lambda函数捕捉列表必须为空。
- e. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
- f. lambda表达式之间不能相互赋值,即使看起来类型相同
(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起
省略
mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修
饰符时,参数列表不可省略(即使参数为空)。
->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分
可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。
因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。
例如使用lambda表达式来实现一个reverse
//因为lambda表达式本质是一个匿名函数,是他自己唯一的未命名类型,所以需要使用auto来进行类型推导
auto reverse = [](string& str){
int begin = 0, end = str.size() - 1;
while (begin < end)
{
std::swap(str[begin++], str[end--]);
}
};
int main()
{
//使用变量名调用即可
string str = "HelloWorld";
reverse(str);
return 0;
}
lambda表达式的原理
下面来砍空lambda表达式在汇编下的调用过程
转入反汇编进行查看,当我们调用lambda表达式时,可以看到编译器在底层调用了lamber_uuid类(uuid为中间的编号)的operator(),这个做法是不是有点眼熟,这就是仿函数的做法。
下面与仿函数进行一个对比
struct Reverse
{
void operator()(string& str)
{
int begin = 0, end = str.size() - 1;
while (begin < end)
{
std::swap(str[begin++], str[end--]);
}
}
};
int main()
{
string str = "HelloWorld";
Reverse()(str);
return 0;
}
无论是从使用上看,还是从底层的汇编代码来看,lambda表达式都与仿函数几乎一模一样,所以由此可以推断出lambda表达式的原理,当定义了lambda表达式之后,编译器按照仿函数的形式自动生成一个lamber_uuid类,并重载其中的operator(),当用户进行调用的时候会自动通过该类的匿名对象来调用,使得其看起来和普通的函数一样。
function模板
C++中可调用对象(如函数指针,仿函数,lambda表达式等)的虽然都有一个比较统一的操作形式,但是定义方法五花八门,这样就导致使用统一的方式保存可调用对象或者传递可调用对象时,会十分繁琐。C++11中提供了std::function和std::bind统一了可调用对象的各种操作。
例如
// 普通函数
int add(int a, int b){return a+b;}
// lambda表达式
auto mod = [](int a, int b){ return a % b;}
// 仿函数
struct divide{
int operator()(int denominator, int divisor){
return denominator/divisor;
}
};
上面的几种不同的可调用对象虽然类型不同,但是根据参数和返回值可以共享同一种调用形式int(int ,int)
通过function就可以统一其调用形式
std::function<int(int ,int)> a = add;
std::function<int(int ,int)> b = mod ;
std::function<int(int ,int)> c = divide();
定义格式:std::function<返回值(参数列表)> 名字
- std::function 是一个可调用对象包装器,是一个类模板,可以容纳除了类成员函数指针之外的所有可调用对象,它可以用统一的方式处理函数、函数对象、函数指针,并允许保存和延迟它们的执行。
- std::function可以取代函数指针的作用,因为它可以延迟函数的执行,特别适合作为回调函数使用。它比普通函数指针更加的灵活和便利。
function与重载函数
在使用function的时候还有一个需要注意的点,就是我们不能将重载过的函数直接放入function对象中,因为会有二义性的问题
例如
int add(int x, int y);
double add(double x, double y);
map<string, function<int(int, int)>> map;
map.insert({"+", add});//此时无法判断是哪个add
//所以此时就不能直接使用函数名进行插入,可以通过使用函数指针来指向对应函数,再通过函数指针插入来消除二义性
int (*func)(int, int) = add;
map.insert({"+", func});
在这里插入代码片
bind函数适配器
对于那些只在少量地方使用的简单操作,使用lambda表达式时非常好的,但是如果使用的地方多了,就应该定义一个函数,而不是多次编写相同的lambda表达式。而库函数提供的模板参数又大多是以一个可调用的对象来进行接收,而如果需要将函数写出仿函数的形式,又增添了不少麻烦,所以C++11提供了一个通用的函数适配器bind,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。(其实就是创建一个对象,将提供的函数作为其operator()的重载)
std::bind将可调用对象与其参数一起进行绑定,绑定后的结果可以使用std::function保存。std::bind主要有以下两个作用:
- 将可调用对象和其参数绑定成一个仿函数;
- 只绑定部分参数,减少可调用对象传入的参数。
bind的语法如下
bind (callable, arg_list);
/*
返回值为绑定完成的可调用对象
参数callable为需要绑定的函数
arg_list为参数列表
*/
下面以reverse举个例子
void reverse(string& str)
{
int begin = 0, end = str.size() - 1;
while (begin < end)
{
std::swap(str[begin++], str[end--]);
}
}
int main()
{
string str = "HelloWorld";
auto func = bind(reverse,std::placeholders::_1);
func(str);
cout << str << endl;
return 0;
}
在arg_list中通常还会包含形如_n的名字,这是位于中,std::placeholders::_n占位符,表示它们占据了传递给callable的第n个参数,_1为第一个,_2为第二个,以此类推。
例如我们可以利用占位符来固定住第2个参数,只让用户传第一个参数
int add(int x, int y)
{
return x + y;
}
int main()
{
auto func = bind(add, std::placeholders::_1, 5);
cout << func(4) << endl;//9
return 0;
}
如果需要绑定成员函数的时候,因为成员函数不会被隐式转换为函数指针,还需要取地址,还需要其指明其来源的对象。
例如
struct Reverse
{
void reverse(string& str)
{
int begin = 0, end = str.size() - 1;
while (begin < end)
{
std::swap(str[begin++], str[end--]);
}
}
};
int main()
{
Reverse r;
auto func = bind(&Reverse::reverse, &r, std::placeholders::_1);
string str = "helloworld";
func(str);
cout << str << endl;
return 0;
}
当需要传递的参数为引用时
bind的那些不是占位符的参数会被拷贝到bind返回的可调用对象中。但是,与lambda类似,有时对有些绑定的参数希望以引用的方式传递,或是要绑定参数的类型无法拷贝。这时候就需要借助到库函数中的ref函数
ostream & print(ostream &os, const string& s, char c)
{
os << s << c;
return os;
}
int main()
{
ostringstream os1;//ostringstream没有拷贝构造,所以不能传拷贝,只能传引用
bind(print, ref(os1), _1, c)
}