lambda表达式(-std=c++11)
泛型算法中有部分算法除了第一个和第二个参数接受迭代器类型来表示范围外,它的第三个参数类型是一个谓词。
谓词是一个可调用表达式,分为一元谓词(只接受一个参数),二元谓词(接受两个参数)。
比如说常用的sort算法,它的第三个参数就是一个二元谓词。
#include <algorithm>
#include <string>
#include <vector>
#include <iostream>
using namespace std;
//定义一个二元谓词使sort算法按字典的逆序排序
bool reDict(const string &s1, const string &s2){
//正常字典排序为s1 < s2
return s1 > s2;
}
int main(void){
vector<string> container = {"a", "b", "c", "d"};
sort(container.begin(), container.end(), reDict);
for(auto i = container.begin();i != container.end();i++){
cout << *i << " ";
}
//d c b a
return 0;
}
以及partition算法,它接受一元谓词参数,并将调用一元谓词返回true的值排在容器的前面,最终返回一个排序后最后一个返回true的值的之后的迭代器。
#include <iostream>
#include <algorithm>
#include <vector>
#include <string>
using namespace std;
//找到vector中大于5的所有字符串
bool size_bigger_than_5(const string &s1){
if(s1.size() >= 5){
return true;
}
else{
return false;
}
}
int main(void){
vector<string> container = {"a", "ab", "abc", "abcd", "abcde", "abcdef"};
auto end = partition(container.begin(), container.end(), size_bigger_than_5);
for(auto i = container.begin();i != end;i++){
cout << *i << endl;
}
//最后输出"abcdef", "abcde"
return 0;
}
因此,谓词限制了传入参数的数量,但有时候我们又希望传入更多的参数,比如说,我们使用partition算法刷选出大于或等于string长度为n的字符串,这个n由输入流读取,此时partition的一元谓词必须读入两个参数,明显不符合一元谓词的定义,所以这时就需要使用lambda函数,多余的参数通过捕获列表传输。
int main(void){
vector<string> container = {"a", "ab", "abc", "abcd", "abcde", "abcdef"};
int size;
cin >> size;
auto end = partition(container.begin(), container.end(),[size](const string &s1){
return s1.size() >= size;
});
for(auto i = container.begin();i != end;i++){
cout << *i << endl;
}
return 0;
}
lambda表达式主要分为四个部分:捕获列表,参数列表,返回类型,函数体。
其中,参数列表和返回类型都可以省略,捕获列表和函数体必须包含。
返回类型
在省略返回类型的时候,lambda表达式会根据函数体的return语句推断返回类型,而如果没有return语句的时候就会返回void。
在C++ primer上的P347的Note的翻译就个人的理解来说比较容易产生歧义:
Note:如果lambda的函数体包含任何单一return语句之外的内容,且未指定返回类型,则返回void。
这里并非指在没有指定返回类型的时候如果函数体出现了return语句之外的语句就会返回void,比如以下情况不会返回void:
auto print = []{
std::string hello = "hello world";
return hello;
};
std::cout << print() << std::endl;
//hello world
而是指如果lambda函数没有出现return语句又没有指定返回类型的时候,lambda就会返回void。
C++ Primer中还举到了一个关于一个lambda中多个return语句但没有指定返回类型的例子(当然如果指定返回类型后多条return语句是没有问题的):
transform(vi.begin(), vi.end(), vi.begin(), [](int i){
if(i > 0) return i;else return -i;
});
//在以前老版本的编译器会报错
然而现在lambda表达式多条return语句已经支持,只要return的是同一类型。参考传送门
传递参数
lambda不支持默认参数。
捕获列表
捕获列表中的变量是要求在在当前函数内的局部变量,如果是全局变量则不需要再通过捕获列表来传递(因此std命名空间内的cout也不需要通过捕获列表来传递)。
#include <iostream>
int global = 88;
int main(void){
int first;
std::cin >> first;
int second;
std::cin >> second;
auto sum = [first](int s){
return global + s + first;
};
//global变量是全局变量,因此不需要传递
//first变量是当前的main函数内的局部变量,因此需要捕获列表
//second变量则作为lambda的参数传进去
std::cout << sum(second) << std::endl;
return 0;
}
lambda表达式实质生成了一个未命名的类类型及其对象,并将对象返回,捕获列表中的变量则作为对象的普通数据成员。
捕获列表的捕获同样分为值捕获和引用捕获。
值捕获
值捕获实质上就是将值赋值给对象的数据成员,这是在对象创建的时候进行的,而并非调用的时候进行,即使外界的值变了,对象的数据成员不变。
如果是值捕获(默认赋值后不会改变对象的数据成员)而我们又想改变对象的数据成员的值,我们可以使用关键字mutable。
#include <iostream>
int main(void){
int change = 11;
//没有关键字mutable编译器将报错
auto fun = [change]() mutable {
change++;
return change;
};
change = 0;
std::cout << fun();//12
return 0;
}
引用捕获
如果调用函数返回一个lambda表达式并引用捕获一个变量,一定要确认该变量的生命周期,比如说该变量是一个函数内的局部变量,那么就会随着函数调用结束被销毁,此时访问lambda表达式对象的数据成员(即引用捕获的值)就会发生不可预料的事情。
隐式捕获
[=]将当前函数的所有局部变量值捕获到lambda中。
[&]将当前函数的所有局部变量引用捕获到lambda 中。
两者可以混搭。
参数绑定
由于谓词的限制,普通函数不能传入过多的参数,因此上面我们通过lambda表达式的捕获列表来传递过多的参数,除此之外,我们还能通过bind函数绑定来实现传递参数。
auto newCallable = bind(callable, arg_list)
其中bind函数定义在functional头文件中,我们现在通过bind函数实现寻找字符串数组中长度大于等于n的字符串
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <functional>
using namespace std;
using namespace std::placeholders;
bool size_bigger_than_n(const string &s1, int n){
return s1.size() >= n;
}
int main(void){
vector<string> container = {"a", "ab", "abc", "abcd"};
int n;
cin >> n;
auto size_wrapper = bind(size_bigger_than_n, _1, n);
//size_wrapper(s1) == size_bigger_than_n(s1, n)
auto end = partition(container.begin(), container.end(), size_wrapper);
for(auto i = container.begin();i != end;i++){
cout << *i << endl;
}
return 0;
}
_1是定义在std命名空间内的placeholders命名空间的占位符,它表示当我们调用size_wrapper()时传进来的第一个位置的参数(因此_1, _2的位置关系都是相对于newCallable而言的),然后它会按照args_list的位置传递给size_bigger_than_n函数。(其中args_list的位置就对应着size_bigger_than_n的参数位置)
我们可以通过调用using声明来使用占位符:
using namespace std::placeholders::_1;
//一劳永逸的方法
using namespace std::placeholders;
bind函数对不是占位符的参数默认会使用值传递,即对传递的参数进行拷贝,但有时候我们会遇到不可拷贝的对象,此时则需要引用传递。
使用函数ref即可返回一个可拷贝的引用对象。