lambda表达式与std::function
在这篇文章中,我们将探讨 lambda 在不同方面的表现。然后我们将研究 std::function 及其工作原理。
什么是lambda
如果你还没用过C++11最强大的特征之一——lambda,我就来做一个简短的介绍:
Lambda是匿名函数的别称。从本质上讲,它们是一种在代码的逻辑位置编写函数(比如回调函数)的简单方法。
我最喜欢的C++表达式是 [](){}();
,它声明了一个空的lambda并且立即执行它。这个表达式显然没有任何功能作用,只是告诉你lambda表达式的格式。更好的一个例子是跟STL结合:
std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; });
与C++98方法相比,它具有以下优点:它是代码在逻辑上的位置(而不是在此范围之外定义类/函数),并且不会污染任何名称空间(尽管即使在C++98中也很容易绕过)。
lambda语法
Lambdas 分为3部分:
[capture]
: 捕获列表 - 捕捉列表总是出现在Lambda函数的开始处。实际上,[]是Lambda引出符。编译器根据该引出符判断接下来的代码是否是Lambda函数。捕捉列表能够捕捉上下文中的变量以供Lambda函数使用;(parameters)
: 参数列表 – 与普通函数的参数列表一致。如果不需要参数传递,则可以连同括号“()”一起省略;{statement}
: 函数体 – 内容与普通函数一样,不过除了可以使用参数之外,还可以使用所有捕获的变量。
举个例子:
int i = 0, j = 1;
auto func = [i, &j](bool b, float f){ ++j; cout << i << ", " << b << ", " << f << endl; };
func(true, 1.0f);
- 第一行很简单 - 创建两个
int
变量 命名为i
和j
. - 第二行定义了一个lambda表达式:
- 通过值传递捕捉变量
i
,通过引用传递捕捉变量j
- 接受2个参数:
bool b
和float f
, - 调用时打印
b
和f
- 通过值传递捕捉变量
- 第三行用
true
和1.0f
为参数调用lambda.
我们可以把lambda表达式看做类:
- 捕捉列表是数据成员:
func
的数据成员是i
和j
;- lambda可以在其代码范围内访问这些成员.
- 创建lambda时,构造函数将捕获的变量复制到数据成员;
- 这个类有
operator()(...)
(对于func
来说...
就是bool, float
); - 它有一个作用域生存期和一个释放成员的析构函数.
语法方面的最后一点:你还可以指定默认捕获:
-
[var]表示值传递方式捕捉变量var;
-
[=]表示值传递方式捕捉所有父作用域的变量(包括this);
-
[&var]表示引用传递捕捉变量var;
-
[&]表示引用传递方式捕捉所有父作用域的变量(包括this);
-
[this]表示值传递方式捕捉当前的this指针。
上面提到了一个父作用域,也就是包含Lambda函数的语句块,说通俗点就是包含Lambda的“{}”代码块。上面的捕捉列表还可以进行组合,例如:
- [=,&a,&b]表示以引用传递的方式捕捉变量a和b,以值传递方式捕捉其它所有变量;
- [&,a,this]表示以值传递的方式捕捉变量a和this,引用传递方式捕捉其它所有变量。
不过值得注意的是,捕捉列表不允许变量重复传递。下面一些例子就是典型的重复,会导致编译时期的错误。例如:
- [=,a]这里已经以值传递方式捕捉了所有变量,但是重复捕捉a了,会报错的;
- [&,&this]这里&已经以引用传递方式捕捉了所有变量,再捕捉this也是一种重复。
值传递VS引用传递
上面我们提到了通过值和通过引用捕获lambda。有什么区别?下面是一个简单的代码,可以说明:
int i = 0;
auto foo = [i](){ cout << i << endl; };
auto bar = [&i](){ cout << i << endl; };
i = 10;
foo();
bar();
输出结果:
0
10
可以看出,值传递传入的是值,如果传入的是一个变量,相当于传递了一个副本,不会改变原有变量。引用传递传递的是一个指针(c++里也有引用),会改变原变量的值。
lambda作用域
所有捕捉到的变量作用域都在lambda范围内:
#include
#include
struct MyStruct {
MyStruct() { std::cout << "Constructed" << std::endl; }
MyStruct(MyStruct const&) { std::cout << "Copy-Constructed" << std::endl; }
~MyStruct() { std::cout << "Destructed" << std::endl; }
};
int main() {
std::cout << "Creating MyStruct..." << std::endl;
MyStruct ms;
{
std::cout << "Creating lambda..." << std::endl;
auto f = [ms](){}; // note 'ms' is captured by-value
std::cout << "Destroying lambda..." << std::endl;
}
std::cout << "Destroying MyStruct..." << std::endl;
}
输出:
Creating MyStruct...
Constructed
Creating lambda...
Copy-Constructed
Destroying lambda...
Destructed
Destroying MyStruct...
Destructed
mutable
lambda
lambda的 operator()
默认是const, 这意味着它不能直接修改捕捉到的变量. 要想修改的话需要添加 mutable
:
int i = 1;
[&i](){ i = 1; }; // ok, 'i' 是引用传递捕捉到的.
[i](){ i = 1; }; // ERROR: 'i'是只读变量.
[i]() mutable { i = 1; }; // ok.
lambda可以直接复制,就像类一样:
int i = 0;
auto x = [i]() mutable { cout << ++i << endl; }
x();
auto y = x;
x();
y();
输出:
1
2
2
lambda表达式的大小
因为lambda有捕获,所以lambda没有固定大小。举个例子:
auto f1 = [](){};
cout << sizeof(f1) << endl;
std::array ar;
auto f2 = [&ar](){};
cout << sizeof(f2) << endl;
auto f3 = [ar](){};
cout << sizeof(f3) << endl;
输出 (64位下):
1
8
100
性能
Lambda在性能方面也非常出色。因为它们是对象而不是指针,所以编译器可以很容易地内联它们,就像仿函数一样。这意味着多次调用lambda(例如使用std::sort
或std::copy_if
)比使用全局函数要好得多。这是C++的实际速度比C快的一个例子。
std::function
std::function
是一个模板化对象,用于存储和调用任何可调用类型,例如函数、对象、lambda 和 std::bind
的结果。
举例
#include
#include
using namespace std;
void global_f() {
cout << "global_f()" << endl;
}
struct Functor {
void operator()() { cout << "Functor" << endl; }
};
int main() {
std::function f;
cout << "sizeof(f) == " << sizeof(f) << endl;
f = global_f;
f();
f = [](){ cout << "Lambda" << endl;};
f();
Functor functor;
f = functor;
f();
}
输出:
$ clang++ main.cpp -std=c++14 && ./a.out
sizeof(f) == 32
global_f()
Lambda
Functor