回头看系列文章序
自大三起我认识到,随着应用知识的扩展,基础知识的重要性愈加明显。时至今日,已经到了无法忽视的地步,以至于我们必须采取有效措施,认真巩固语言基础、数据库、操作系统等一系列重要基础知识,将零散的知识点体系化,我将这一计划称之为“回头看”。

C++ 11引入了lambda表达式,用于定义匿名函数,相比一般函数,lambda表达式定义方式更简洁,且允许在函数内部定义。Lambda 通常用于封装传递给算法或异步函数的少量代码行。本文描述内容基于C++11标准,后续变更请参考C++官方文档。

此外,我们还经常在Qt编程中接触lambda表达式,lambda的存在大大简化了定义槽函数的过程,lambda表达式还允许你在需要回调的地方内联定义回调逻辑、捕获局部变量,这些能力让Qt大大简化,变得简洁易读。

定义lambda表达式

官方文档中的语法描述(后续版本语法参考文末链接):
没有显示模板形参的lambda表达式-glppiume.png

示例

auto plus = [] (int v1, int v2) -> int { return v1 + v2; }
int sum = plus(1, 2);

lambda 表达式的语法组成

lambda语法组成-zpohawab.png

  1. captures捕获列表:它指定捕获哪些变量,以及捕获是通过值还是通过引用进行的;
  2. params参数列表(可选):类似于函数的参数列表;
  3. mutable 规范(可选);
  4. exception-specification异常选项(可选);
  5. trailing-return-type返回值类型(可选):一般可以省略掉,由编译器来推导;
  6. Lambda body:可以包含普通函数或成员函数体中允许的任何内容。

捕获列表

捕获列表是lambda表达式最大的特性之一,它指定捕获哪些变量,以及捕获是通过值还是通过引用进行的。

常用捕获列表形式示例如下:

  • [] 不捕获任何变量;
  • [&] 捕获外部作用域中的所有变量,通过引用捕获;
  • [=] 捕获外部作用域中的所有变量,通过捕获,只读;
  • [bar] 通过值捕获bar变量,不捕获其他变量
  • [this] 捕获当前类中this指针,让lambda表达式拥有和当前类成员函数同样的访问权限
  • [=, &a] 通过值捕获外部作用域中的所有变量,并且指定通过引用捕获外部变量a;
  • [&, a] 通过引用捕获外部作用域中的所有变量,并且指定通过值捕获外部变量a.

注意
捕获列表中变量的生命周期必须大于lambda的生命周期,否则会导致未定义行为。按值捕获不会修改外部变量,按引用捕获可以修改外部变量。

mutable 规范

通常情况下,lambda表达式捕获的变量是只读的,这意味着我们不能在lambda表达式内部修改这些变量。但某些时候,我们希望在lambda内部修改捕获的变量,此时mutable就可以解决这个问题。

例如这样一个情景:

在这个例子中,x是按值捕获的,意味着lambda表达式内部有一个x的副本。由于默认情况下lambda表达式是不可变的,尝试修改捕获的变量会导致编译错误。

int x = 10;
auto lambda = [x]() {
    x++; // 错误:不能修改按值捕获的变量
};
lambda();

如果我们使用mutable选项,这时便可以对捕获的x副本进行自增处理,并且不影响原值:

int x = 10;
auto lambda = [x]() mutable {
    x++; // 正确:可以修改按值捕获的变量副本
    std::cout << x << std::endl; // 输出11
};
lambda();
std::cout << x << std::endl; // 输出10

也许是没有用到的原因,我总觉得这玩应略微有点鸡肋。

异常选项

异常选项是指通过noexcept说明符声明一个lambda表达式是否会抛出异常。这与普通函数的异常说明符(C++异常规范 )类似。

作为一个可选选项,省略noexcept的默认情况下,编译器不会进行任何关于异常安全性的假设,允许排除异常,但缺点是编译器必须考虑处理异常的开销,可能会影响一些优化。

  • 使用noexcept基本语法如下:
auto noThrowLambda = []() noexcept { body }
// 这个lambda不会抛出异常
  • 允许抛出异常:
auto mayThrowLambda = [](int x) noexcept(false) {
    if (x == 0) {
        throw std::runtime_error("Division by zero");
    }
    return 10 / x;
};

try {
    mayThrowLambda(0);
} catch (const std::runtime_error& e) {
    std::cout << "Caught exception: " << e.what() << std::endl;
}

/*
在本例中,mayThrowLambda被声明为noexcept(false),这意味着它可能抛出异常。如果传递的参数为0,它会抛出一个std::runtime_error异常。
*/
  • 条件noexcept,根据某个条件来决定是否抛出异常:
auto conditionalLambda = [](int x) noexcept(noexcept(10 / x)) {
    return 10 / x;
};

try {
    conditionalLambda(0);
} catch (...) {
    std::cout << "Exception caught!" << std::endl;
}

/*
在这个例子中,conditionalLambda的noexcept说明符是条件性的,它取决于表达式10 / x是否会抛出异常。如果x为0,则表达式会抛出异常,noexcept为false;否则,noexcept为true。
*/

参数列表

Lambda表达式的参数列表和普通函数类似,可以接受输入参数。作为可选列表,若无需要可连带括号省去。

C++14中,如果参数类型是泛型,可以使用auto关键字作为类型说明符。例如:

auto y = [] (auto first, auto second)
{
    return first + second;
};

返回类型

返回类型可以省略,编译器会自动推导。

如果 Lambda 体仅包含一个返回语句,则可以省略 Lambda 表达式的 return-type 部分。 或者,在表达式未返回值的情况下。 如果 lambda 体包含单个返回语句,编译器将从返回表达式的类型推导返回类型。 否则,编译器会将返回类型推导为 void。 下面的示例代码片段说明了这一原则:

auto x1 = [](int i){ return i; }; // 正确: 返回值为 int
auto x2 = []{ return{ 1, 2 }; };  // 错误: 返回值类型推导为 void
// braked init列表中的返回类型无效

编译过程

当编译器遇到一个lambda表达式时,他会将其转换为一个匿名的类,这个类包含了lambda表达式的所有必要信息和功能:

  • 捕获列表:lambda表达式中捕获的变量会成为这个类的成员变量。
  • operator():这个类会重载函数调用运算符operator(),其中包含lambda表达式的实际代码。

用法实例

参考阅读