C++11中lambda、std::function和std:bind详解
前⾔
在C++11新标准中,语⾔本⾝和标准库都增加了很多新内容,本⽂只涉及了⼀些⽪⽑。不过我相信这些新特性当中有⼀些,应该成为所有C++开发者的常规装备。本⽂主要介绍了C++11中lambda、std::function和std:bind,下⾯来⼀起看看详细的介绍吧。
lambda 表达式
C++11中新增了lambda 表达式这⼀语⾔特性。lambda表达式可以让我们快速和便捷的创建⼀个”函数”。
下⾯是lambda表达式的语法:
[ capture-list ] { body }
[ capture-list ] ( params ) { body }
[ capture-list ] ( params ) -> ret { body }
[ capture-list ] ( params ) mutable exception attribute -> ret { body }
这其中:
- capture-list 是需要捕获的变量列表,⽤逗号分隔。其详细说明见下⽂。
- params 是lambda表达式需要的参数列表,写法和函数参数⼀样,不过这⾥不⽀持默认参数。
- ret 指明了lambda表达式的返回值。通过return语句,如果编译器能够推断出返回值的类型。或者表达式没有返回值,“-> ret”可以省略。
- body 函数体。
- mutable 当捕获列表是以复制(见下⽂)的形式捕获时,默认这些复制的值是const的,除⾮指定了mutable。
- exception 提供了异常的说明。
- attribute 对于attribute的描述可以参见这⾥:en.cppreference/w/cpp/language/attributes,这⾥不多说明。
下⾯,我们通过经典的Hello World⽰例来看⼀下lambda表达式:
auto lambda1 = [] {std::cout << "Hello, World!\n";};
lambda1();
这个lambda表达式将打印出字符串“Hello, World!”。
同时,我们将这个表达式赋值给“lambda1”这个变量,然后像调⽤函数⼀样,调⽤这个lambda表达式。
使⽤lambda表达式,可以让我们省却定义函数的⿇烦,以inline的⽅式写出代码,这样的代码通常更简洁。
并且,由于阅读代码时不⽤寻函数定义,这样的代码也更易读。
下⾯,我们来看另外⼀个例⼦。这个例⼦的需求是:
分两次,打印出⼀个vector集合中,所有:
1. 模 5 = 0
2. ⼤于 20
的数字。
现假设已有这个集合的定义如下:
vector<int> numbers { 1, 2, 3, 4, 5, 10, 15, 20, 25, 35, 45, 50 };
我们最先想到的⽅法⾃然是定义两个函数,分别按照上⾯的要求打印出需要的数字,它们的定义如下:
void printNumber1(vector<int>& numbers) {
for (const int& i : numbers) {
if (i % 5 == 0) {
cout<<i<<endl;
}
}
}
void printNumber1(vector<int>& numbers) {
for (const int& i : numbers) {
cout<<i<<endl;
}
}
}
然后,我们在需要的地⽅,调⽤它们:
printNumber1(numbers);
printNumber2(numbers);
这⾥逻辑上并没有问题,但是:
1. 这⾥我们必须先定义这个函数,才能使⽤。⽽这样的函数,可能实际上我们只会使⽤⼀次。
2. 当⼯程⼤到⼀定程度,我们可能不记得每个函数的实现(所以函数命名很重要,原谅我这⾥给函数起了很含糊的名字,你在实际上⼯程中,请不要这样做),为了知道每个函数的实现,我们不得不查看函数的定义,这⽆疑给代码的阅读造成了⼀定的⿇烦。
下⾯,我们来看看使⽤lambda表达式如何改善上⾯说的问题。
使⽤lambda表达式,我们可以这样写:
for_each(numbers.begin(), d(), [] (int i) {
if(i % 5 == 0) {
cout<<i<<endl;
}
});
for_each(numbers.begin(), d(), [] (int i) {
function怎么记忆if(i > 20) {
cout<<i<<endl;
}
});
这⾥,我们不⽤单独定义函数,直接以inline的⽅式解决了问题。并且,这段代码⼀⽓呵成,你很直观的看到了执⾏的逻辑。下⾯,我们再详细看⼀下lambda表达式中的捕获列表的语法,它可能是以下⼏种情况中的⼀种:
[] 不捕获任何变量
[&] 以引⽤的⽅式捕获所有变量
[=] 以复制的⽅式捕获所有变量
[=, &foo] 以引⽤的⽅式捕获foo变量,但是以复制的⽅式捕获其他变量
[bar] 以复制的⽅式捕获bar变量,不再捕获任何其他变量
[this] 捕获this指针
下⾯,我们再以⼀个例⼦说明捕获列表的⽤法。
这⾥,我们的需求是:
打印出⼀个vector<int>的所有数字之和
同样的,我们先以函数的⽅式来解决这个问题,这个函数的定义可以是这样的:
void printSum(vector<int>& numbers) {
int sum = 0;
for (const int& i : numbers) {
sum += i;
}
cout<<sum<<endl;
}
然后,我们在需要的地⽅调⽤这个函数:
vector<int> numbers { 1, 2, 3, 4, 5, 10, 15, 20, 25, 35, 45, 50 };
printSum (numbers);
⽽假设我们⽤lambda表达式来写,这样写就可以了:
vector<int> numbers { 1, 2, 3, 4, 5, 10, 15, 20, 25, 35, 45, 50 };
int sum = 0;
std::for_each(numbers.begin(), d(), [&sum] (const int& i) { sum += i;});
这⾥,我们⽤ [&sum]以引⽤的形式捕获了sum这个变量,并且在lambda表达式中修改了这个变量。
这样写,是不是⽐定义函数的⽅式简洁了很多?
对于这种,能够捕获其定义时上下⽂变量的函数,我们称之为“闭包”,下⽂还将提到。
std::function
上⽂中,对于分两次,打印出⼀个vector集合中,所有:
1. 模 5 = 0
2. ⼤于 20
的数字。
这个需求,我们的实现其实还不够好。
回头看⼀下printNumber1和printNumber2这两个函数,这两个函数⼤部分都是重复的:它们都需要遍历集合,都需要做if判断,然后打印出结果。
实际上,我们在项⽬中经常遇到这个的问题:
两(多)个函数,有⼤部分的代码都是⼀样的,其中只有⼀两⾏代码有不⼀样的地⽅。
其实,我们可以对这个不⼀样的地⽅,再做⼀个抽象,把它们共通起来。
具体到这个例⼦就是:⽆论是“模 5 = 0”还是“⼤于 20”都是满⾜“某种条件”。
⽽很⾃然的会想到,我们是否可以通过⼀个类似这样的函数来做这个判断:
bool func(int i)
然后实现两个函数,通过函数指针的形式来完成判断就好了。
但是,我们马上⼜意识到,这两个函数会很⼩,并且也是只会⽤⼀遍⽽已,定义⼀个函数⼜太“浪费”了。很⾃然的,我们就会想lambda。但是,lambda似乎没法转成函数指针。。。
C++11中,提供了⼀个通⽤的描述⽅法,就是std::function。 std::function可以hold住任何可以通过“()”来调⽤的对象,包括:
普通函数
成员函数
lambda
std::bind(见下⽂)后的结果
std::function的语法是这样:
template <class Ret, Args> class function<)>;
例如:function<bool (int)> filter就表达了我们前⾯需要的那个函数:这个函数接受⼀个int值作为参数,同时返回⼀个bool作为判断的结果。但同时,我们可以⽤lambda表达式直接传递进去。
因此,上⾯的代码可以改写成这样:
void printNumber(vector<int>& number, function<bool (int)> filter) {
for (const int& i : number) {
if (filter(i)) {
cout<<i<<endl;
}
}
}
然后在需要的地⽅,这样调⽤即可:
printNumber(numbers, [] (int i){ return i % 5 == 0;});
printNumber(numbers, [] (int i){ return i > 20;});
这种做法,是不是⼜简洁了不少?
前⾯提到了“闭包”这个词,这⾥我们来聊⼀下闭包。
下⾯是维基百度对于闭包的定义:
在计算机科学中,闭包(英语:Closure),⼜称词法闭包(Lexical Closure)或函数闭包(function closures),是引⽤了⾃由变量的函数。这个被引⽤的⾃由变量将和这个函数⼀同存在,即使已经离开了创造它的环境也不例外。
简单来说:闭包可以记忆住创建它时候的那些变量。
下⾯,我们再通过⼀个例⼦来说明。
现在,假设我们的需求是:获取⼀个集合中最⼩和最⼤值,并在稍后的时候(可能是另外⼀个函数中)打印它们。这⾥,我们常规的做法通常是:通过⼀个函数获取集合的最⼤,最⼩值,然后保存住,最后在需要的时候访问这两个值,然后打印它们。
这样做就会需要解决:如果保存和传递最⼤,最⼩这两个值。
但实际上,这⾥我们可以考虑⽤闭包来实现这个功能,让闭包把最⼤,最⼩两个值捕获下来,然后在需要的地⽅调⽤就可以了。
请看⼀下下⾯这段代码:
void getMinMax(vector<int>& number, function<void ()>& printer) {
int min = number.front();
int max = number.front();
for (int i : number) {
if (i < min) {
min = i;
}
if (i > max) {
max = i;
}
}
printer = [=] () {
cout << "min:" <<min<< endl;
cout << "max:" << max << endl;
};
}
这⾥,我们通过function<void ()>& printer(如果你看不懂function,请看上⽂)传递出这个闭包。然后,在需要的地⽅,这样即可:
function<void()> printer;
getMinMax(numbers, printer);
......
printer();
这⾥的printer其实是我们前⾯从getMinMax函数出传出的闭包,这个闭包捕获了min和max。我们直接传递这个闭包给需要的地⽅使⽤,⽽不⽤传递裸的两个数值,是不是优雅的不少?
std::bind
下⾯,我们再改进⼀下需求,假设我们要
打印出vector<int>中,20<x<40范围内的值,该怎么办?
毕竟,bool isBetween(int i, int min, int max) 这个函数可没法对应上
function<bool (int)> filter 啊!参数数量就不⼀样嘛。
这个时候,我们可以⽤ std::bind 。
std::bind的语法是这样的:
template <class Fn, Args> bind (Fn&& fn, Args&&... args);
template <class Ret, class Fn, Args> bind (Fn&& fn, Args&&... args);
std::bind可以将调⽤函数时的部分参数先指定好,留下⼀部分在真正调⽤的时候确定。
(当然,你也可以直接指定全部参数,在调⽤时不再指定。)
这⾥,isBetween中,最⼩,最⼤值其实我们是确定了的,即:20和40。⽽不确定的,其实是真正待判断的数字本⾝,那么我们就可以这么做:
std::bind(isBetween, placeholders::_1, 20, 40);
placeholders::_1的意思是,这⾥是⼀个占位符,在调⽤的时候,将实际传递的第⼀个参数放到这⾥。
占位符的数量可以是任意多的,像这样:
std::placeholders::_1, std::placeholders::_2, …, std::placeholders::_N。
于是乎,对于打印出vector<int>中,20<x<40范围内的值这个需求,我们在不修改printNumber函数的基础上,通过定义⼀个isBetween函数:
bool isBetween( int i, int min, int max) {
return i >= min && i <= max;
}
然后,再这样就搞定了:
function<bool(int)> filter = std::bind(isBetween, placeholders::_1, 20, 40);
printNumber(numbers, filter);
当然,你甚⾄可以直接把这⾥的两⾏写成⼀⾏。
如果你不明⽩这段代码,请再看⼀下printNumber函数的定义:
void printNumber(vector<int>& number, function<bool (int)> filter) {
for (const int& i : number) {
if (filter(i)) {
cout<<i<<endl;
}
}
}
这⾥其实调⽤了filter(i)这个函数对象,⽽这个函数对象只接受⼀个int值作为参数,然后返回⼀个bool值。
function<bool(int)> filter = std::bind(isBetween, placeholders::_1, 20, 40);
绑定之后,只缺⼀个int型参数,所以正好对应得上。
如果不过瘾,我们再来看⼀个bind的例⼦。
我们常常需要在程序中,调⽤⼀些⽤户传过来的回调函数。⽽在回调函数中,⽤户常常会需要记录⼀些状态,于是常常希望通过⼀个对象的成员函数传给过来作为回调函数。但是在C++中,这样做是很⿇烦的⼀个事情。因为,回调函数的类型我们很难定义。但是,结合std::function和std::bind,⼀切变得容易多了。结合前⾯的例⼦,现在就假设我们的回调函数是需要打印集合中的最⼤,最⼩值。
这⾥假设我们是通过⼀个类来记录和打印值的,这个类的定义是这样的:
class Printer {
private:
int min, max;
public:
Printer(int x, int y) {
min = x;
max = y;
}
void print() {
cout << "min:" << min << endl;
cout << "max:" << max << endl;
}
};
由于回调函数不需要参数,因此使⽤回调函数的代码是这样的:
void usingCallback(function<void ()> print) {
print();
}
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论