什么是C++11中的lambda表达式?我什么时候会用到?它们能解决哪一类问题,而在它们被引入之前是不可能的?
举几个例子和用例会比较有用。
C++包含有用的通用函数,如std::for_each
和std::transform
,它们可以非常方便。不幸的是,它们的使用也可能相当麻烦,特别是当你想应用的functor对特定函数来说是唯一的。
#include <algorithm>
#include <vector>
namespace {
struct f {
void operator()(int) {
// do something
}
};
}
void func(std::vector<int>& v) {
f f;
std::for_each(v.begin(), v.end(), f);
}
如果你只在特定的地方使用一次`f',那么为了做一些微不足道的事情而编写一个完整的类就显得有些多余了。
在C++03中,你可能会想写一些像下面这样的东西,以保持functor的局部性。
void func2(std::vector<int>& v) {
struct {
void operator()(int) {
// do something
}
} f;
std::for_each(v.begin(), v.end(), f);
}
然而这是不允许的,在C++03中,`f'不能被传递给模板函数。
C++11引入了lambdas,允许你写一个内联的、匿名的functor来代替struct f
。对于简单的小例子来说,这可以更干净地阅读(它把所有东西都放在一个地方),而且可能更简单地维护,例如在最简单的形式下。
void func3(std::vector<int>& v) {
std::for_each(v.begin(), v.end(), [](int) { /* do something here*/ });
}
Lambda函数只是匿名向量的语法糖。
在简单的情况下,lambda的返回类型是为你推导出来的,例如。
void func4(std::vector<double>& v) {
std::transform(v.begin(), v.end(), v.begin(),
[](double d) { return d < 0.00001 ? 0 : d; }
);
}
然而,当你开始写更复杂的lambdas时,你会很快遇到编译器无法推导出返回类型的情况,例如。
void func4(std::vector<double>& v) {
std::transform(v.begin(), v.end(), v.begin(),
[](double d) {
if (d < 0.0001) {
return 0;
} else {
return d;
}
});
}
为了解决这个问题,你可以使用"-> T "来明确指定λ函数的返回类型。
void func4(std::vector<double>& v) {
std::transform(v.begin(), v.end(), v.begin(),
[](double d) -> double {
if (d < 0.0001) {
return 0;
} else {
return d;
}
});
}
到目前为止,除了传递给lambda的内容外,我们还没有使用其他东西,但我们也可以在lambda中使用其他变量。如果你想访问其他变量,你可以使用捕获子句(表达式的[]
),到目前为止,在这些例子中还没有使用,例如。
void func5(std::vector<double>& v, const double& epsilon) {
std::transform(v.begin(), v.end(), v.begin(),
[epsilon](double d) -> double {
if (d < epsilon) {
return 0;
} else {
return d;
}
});
}
你可以通过引用和值来捕获,你可以分别用&
和=
来指定。
[&epsilon]
通过引用捕获[&]
通过引用捕获lambda中使用的所有变量[=]
通过值捕获lambda中使用的所有变量[&, epsilon]
捕获与[&]类似的变量,但ε是按值计算的。[=, &epsilon]
捕获与[=]类似的变量,但通过引用获得epsilon。生成的operator()
默认为const',这意味着当你访问它们时,默认捕获的变量将是
const'。这样做的结果是,每次调用相同的输入都会产生相同的结果,但是你可以把lambda标记为`mutable',要求生成的operator()
不是`const'。
lambda函数的C++概念起源于lambda微积分和函数式编程。lambda是一个未命名的函数,它对那些不可能重用且不值得命名的短代码片段很有用(在实际编程中,而不是理论上)。
在C++中,lambda函数的定义是这样的
[]() { } // barebone lambda
或在其所有的荣耀中
[]() mutable -> T { } // T is the return type, still lacking throw()
[]
是捕获列表,()
是参数列表,{}
是函数体。
捕获列表定义了来自lambda外部的东西应该在函数体中使用,以及如何使用。 它可以是这样的
你可以在逗号分隔的列表"[x, &y]"中混合上述任何一种。
参数列表与其他C++函数的参数列表是一样的。
当lambda被实际调用时将被执行的代码。
如果一个lambda只有一个返回语句,返回类型可以被省略,并具有decltype(return_statement)
的隐含类型。
如果一个lambda被标记为mutable(例如:[]() mutable { }
),那么它就被允许改变被value捕获的值。
ISO标准定义的库从lambdas中受益匪浅,并且提高了可用性,因为现在用户不必在一些可访问的范围内用小的functors扰乱他们的代码。
在C++14中,lambdas被各种建议所扩展。
捕获列表中的一个元素现在可以用=
来初始化。这允许重命名变量并通过移动来捕获。一个来自标准的例子。
int x = 4;
auto y = [&r = x, x = x+1]()->int {
r += 2;
return x+2;
}(); // Updates ::x to 6, and initializes y to 7.
和一个来自维基百科的例子,展示了如何用std::move
捕获。
auto ptr = std::make_unique<int>(10); // See below for std::make_unique
auto lambda = [ptr = std::move(ptr)] {return *ptr;};
现在,Lambdas可以是通用的(如果T
是一个类型模板参数,那么auto
就相当于这里的T
)。
T
是周围作用域中的一个类型模板参数)。)
auto lambda = [](auto x, auto y) {return x + y;};
C++14允许对每个函数的返回类型进行推导,并且不限制于 "return expression; "形式的函数。这也被扩展到lambdas。
Lambda表达式通常用于封装算法,以便将其传递给另一个函数。 然而,可以在定义后立即执行Lambda。
[&](){ ...your code... }(); // immediately executed lambda expression
在功能上等同于
{ ...your code... } // simple code block
这使得lambda表达式成为重构复杂函数的强大工具**。 如上图所示,你可以先将一个代码部分包裹在lambda函数中。 然后,显式参数化的过程可以逐步进行,每一步之后都有中间的测试。 一旦你将代码块完全参数化(如去除&
所示),你就可以将代码移到外部位置,使其成为一个正常的函数。
同样地,你可以使用lambda表达式来根据算法的结果来初始化变量...
int a = []( int b ){ int r=1; while (b>0) r*=b--; return r; }(5); // 5!
作为划分程序逻辑的**方式,你甚至会发现将一个lambda表达式作为参数传递给另一个lambda表达式是非常有用的......
[&]( std::function<void()> algorithm ) // wrapper section
{
...your wrapper code...
algorithm();
...your wrapper code...
}
([&]() // algorithm section
{
...your algorithm code...
});
兰姆达表达式还允许你创建命名的嵌套函数,这可能是避免重复逻辑的一种方便方式。 当把一个非简单的函数作为参数传递给另一个函数时,使用命名的lambdas也往往会让人觉得更容易接受(与匿名的内联lambdas相比)。 *注意:不要忘了大括号后面的分号。
auto algorithm = [&]( double x, double m, double b ) -> double
{
return m*x+b;
};
int a=algorithm(1,2,3), b=algorithm(4,5,6);
如果随后的剖析发现该函数对象的初始化开销很大,你可能会选择将其改写为普通函数。