Lambdas In C++11
Lambdas
好吧,所以C ++中的lambdas很酷,而且我们一直在编码,一只手臂绑在我背后。C ++ 11为我们带来了这种奇妙的善良,这很棒,但你是如何实际使用它们的呢?没有搞乱,让我们跳进去吧! 一个简单的例子 考虑想要在容器中找到第一个负值的简单任务。为了演示,我们假设您想要使用STL的std::find_if算法,该算法需要函数或类似函数的对象作为其参数之一来定义要搜索的条件。典型的实现可能是:
std::vector<int> values = {23, -5, -2, 16, 20};
bool isNegative(int v)
{
return v < 0;
}
auto pos = std::find_if(
values.begin(),
values.end(),
isNegative);
必须将isNegative()操作定义为单独的函数可能有点令人讨厌,这是lambda可能有用的地方。lambda函数允许在调用std::find_if点处直接写入调用的条件,而不必定义单独的命名函数。这可以非常方便,并允许代码在本地定义到使用它的位置。
auto pos = std::find_if(
values.begin(),
values.end(),
[](int v) { return v < 0; } );
这个相当简单的例子并没有真正挖掘lambdas的一些更强大的优势,但它确实突出了有多少人第一次遇到它们。要真正了解如何利用lambdas获得良好效果,我们首先需要了解它们的语法。
Lambda语法 lambda函数的(略微简化的)一般形式是:
[captures] (parameters) -> returnType {body}
前导[]是告诉编译器这是一个lambda函数。但是,与常规函数不同,lambdas没有名称,但如果将lambda赋给变量,则可以将其视为常规命名函数。或者(也许更常见),lambdas可以直接定义为调用时的函数参数,就像我们之前展示的简单示例一样。使用lambda定义的参数与常规函数参数相同,而body实际上就像普通函数的主体一样。该返回类型是相当于普通函数的返回类型,但是当编译器可以推断返回类型(即只return在声明中体),它可以省略。抛开语法的捕获部分片刻,lambdas的一些示例有助于说明如何定义和使用它们。
编译器推导的返回类型(double),其中lambda分配给变量。然后该变量用于调用lambda函数:
auto squared = [](double t) { return t*t; };
std::cout << "5 squared = " << squared(5) << std::endl;
具有多个参数的Lambda和明确定义的返回类型:
auto addTrunc = [](double a, double b) -> int { return a+b; };
std::cout << addTrunc(4.3, 1.2) << std::endl;
计算可被5整除的值的数量,并将lambda定义为调用的一部分count_if:
std::vector<int> values = {23, -5, -2, 16, 20};
auto c = std::count_if(values.begin(),
values.end(),
[](int i) { return i == ((i/5)*5); });
std::cout << "Result = " << c << std::endl;
捕获 上面的例子相当简单。lambda的真正力量来自于它们在定义lambda的封闭范围内使用变量和类型的能力。[captures]lambda语法的一部分只是一个逗号分隔的主体可以引用的东西列表。这些可以是单个变量,this指针或捕获默认值。
个别捕获变量 lambda可以在定义lambda的点捕获特定变量的值。这可以通过以下两种方式之一完成:
通过在定义lambda的位置复制变量的值来捕获变量的值。如果变量稍后更改值,则lambda仍使用原始值。只需使用变量名称在捕获列表中指定复制捕获。 通过引用捕获变量,这意味着在执行lambda时检索变量的值,而不是在定义时检索。通过在捕获列表中的变量名称前放置一个与号(&)来指定引用捕获。 以下示例说明了不同之处:
int x = 5;
auto copyLambda = [x](){ return x; };
auto refLambda = [&x](){ return x; };
std::cout << copyLambda() << std::endl;
std::cout << refLambda() << std::endl;
x = 7;
std::cout << copyLambda() << std::endl;
std::cout << refLambda() << std::endl;
这段代码的输出将是:
5
5
5
7
允许通过副本和引用混合捕获不同的变量,但是您不能多次捕获特定变量:
auto lam1 = [x,&y](){ return x*y; }; // OK
auto lam2 = [&x,x](){ return x*x; }; // Error: x captured twice
C ++ 14扩展了单个变量捕获的功能,但我们不会在本文中讨论它们。
捕获this指针 当在成员函数内定义lambda时,它还能够访问该类的成员。通过this以类似于通过复制捕获变量的方式捕获指针来给予lambda访问权限:
class Foo
{
int x;
public:
Foo() : x(10) {}
void bar()
{
// Increment x every time we are called
auto lam = [this](){ return ++x; };
std::cout << lam() << std::endl;
}
};
Foo foo;
foo.bar(); // Outputs 11
foo.bar(); // Outputs 12
捕获this指针特别方便,lambdas经常使用此功能。请注意,this通过引用捕获并不真正有意义(您无法更改其值),因此它应始终在捕获语句中显示为按值捕获。
捕获默认值 捕获单个变量和this指针非常有用,但有时默认捕获规则可能更方便。默认捕获可以防止必须在lambda的捕获规范中明确列出正文引用的每个变量。默认捕获以两种方式之一指定:
[=]表示通过复制捕获所有变量 [&]表示通过引用捕获所有变量 您还可以将默认捕获与单个变量捕获混合,但单个变量捕获必须与默认捕获不同(即两者都不能通过逐个捕获或两者都是按引用捕获)。单个捕获就像覆盖默认捕获一样。例如:
int x = 10;
int y = 14;
auto lam1 = []() { return 24; }; // OK: capture nothing
auto lam2 = [=]() { return x+y; }; // OK: copy x, copy y
auto lam3 = [&]() { return x+y; }; // OK: reference x, reference y
auto lam4 = [=,&x]() { return x+y; }; // OK: reference x, copy y
auto lam5 = [&,x]() { return x+y; }; // OK: copy x, reference y
auto lam6 = [&x,=]() { return x+y; }; // Error: default must be first
auto lam7 = [=,x]() { return x+y; }; // Error: both specify copy x
auto lam8 = [=,this]() { return x+y; }; // Error: both specify copy this
auto lam9 = [&,&x]() { return x+y; }; // Error: both specify reference x
在实践中,大多数lambda通常将其捕获规范设置为空(即不捕获任何内容)或仅捕获少量特定捕获。有些人提倡反对使用默认捕获规范,理由是它们可能无意中捕获某些变量,而只捕获特定变量会使事情明确清楚。
现实世界的例子:Qt 使用Qt的人将熟悉其信号槽机制(Boost也有与signal2模块类似的功能)。使用Qt5,可以将lambdas用于插槽,而不必定义单独的函数。这特别有用,因为用于处理UI事件的插槽通常是微不足道的单行。以下缩减代码显示了这样一个示例:
class MyUI : public QDialog
{
QLabel* m_label;
QLineEdit* m_lineEdit;
public:
MyUI();
};
MyUI::MyUI()
{
m_label = new QLabel;
m_lineEdit = new QLineEdit;
// ... other initialisation of UI, etc.
// Whenever the line edit's text changes,
// update the label's text to show the
// uppercase version of that string
connect(m_lineEdit, &QLineEdit::textChanged,
[this](const QString& s) { m_label->setText(s.toUpper()); });
}
感谢Olivier Goffart 在Woboq上发表的这篇以及其他与Qt相关的C ++ 11想法和技巧的有用文章。
闭幕致辞 上面的例子和讨论实际上只是划分了lambdas可以用于的表面。在完整的,充满lambda语法提供了这里不作讨论一些额外的功能,但是上面介绍的材料覆盖了绝大多数的典型用途。使用STL和使用Qt或Boost等工具包支持传递函数或函数对象时,Lambdas特别方便。一旦它们的主体部分长度超过几行,Lambdas开始变得不那么有用了,其中一个单独的命名函数可以使代码更具可读性。Lambdas也可以是一种更有效的实现回调的方式std::function(),更详细的讨论可以在文章OnLeavingScope:The sequel中找到。