【回头看之C++】lambda表达式
回头看系列文章序
自大三起我认识到,随着应用知识的扩展,基础知识的重要性愈加明显。时至今日,已经到了无法忽视的地步,以至于我们必须采取有效措施,认真巩固语言基础、数据库、操作系统等一系列重要基础知识,将零散的知识点体系化,我将这一计划称之为“回头看”。
C++ 11引入了lambda表达式,用于定义匿名函数,相比一般函数,lambda表达式定义方式更简洁,且允许在函数内部定义。Lambda 通常用于封装传递给算法或异步函数的少量代码行。本文描述内容基于C++11标准,后续变更请参考C++官方文档。
此外,我们还经常在Qt编程中接触lambda表达式,lambda的存在大大简化了定义槽函数的过程,lambda表达式还允许你在需要回调的地方内联定义回调逻辑、捕获局部变量,这些能力让Qt大大简化,变得简洁易读。
定义lambda表达式
官方文档中的语法描述(后续版本语法参考文末链接):
示例:
auto plus = [] (int v1, int v2) -> int { return v1 + v2; }
int sum = plus(1, 2);
lambda 表达式的语法组成:
captures
捕获列表:它指定捕获哪些变量,以及捕获是通过值还是通过引用进行的;params
参数列表(可选):类似于函数的参数列表;mutable
规范(可选);exception-specification
异常选项(可选);trailing-return-type
返回值类型(可选):一般可以省略掉,由编译器来推导;- 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表达式的实际代码。