C++ Lambda表达式:语法、特点和原理
介绍
在本文中,我们将介绍 c++ Lambda表达式的定义、用法、特点和原理
通过多个例子,展示了 Lambda表达式的参数、返回值、捕获、引用、修改等方式,以及如何用它们定义匿名函数和算法。
什么是 Lambda表达式
Lambda表达式是一种在被调用的位置或作为参数传递给函数的位置定义匿名函数对象(闭包)的简便方法。Lambda表达式的基本语法如下:
[capture list] (parameter list) -> return type { function body }
其中:
- capture list 是捕获列表,用于指定 Lambda表达式可以访问的外部变量,以及是按值还是按引用的方式访问。
捕获列表为空,表示不访问任何外部变量,
也可以使用默认捕获模式 & 或 = 来表示按引用或按值捕获所有外部变量,
还可以混合使用具体的变量名和默认捕获模式来指定不同的捕获方式。 - parameter list 是参数列表,用于表示 Lambda表达式的参数,可以为空,表示没有参数,也可以和普通函数一样指定参数的类型和名称,
还可以在 c++14 中使用 auto 关键字来实现泛型参数。 - return type 是返回值类型,用于指定 Lambda表达式的返回值类型,可以省略,表示由编译器根据函数体推导,也可以使用 -> 符号显式指定,
还可以在 c++14 中使用 auto 关键字来实现泛型返回值。 - function body 是函数体,用于表示 Lambda表达式的具体逻辑,可以是一条语句,也可以是多条语句,
还可以在 c++14 中使用 constexpr 来实现编译期计算。
Lambda表达式的四种捕获方式
- 值捕获(capture by value):在捕获列表中使用变量名,表示将该变量的值拷贝到 Lambda 表达式中,作为一个数据成员。
值捕获的变量在 Lambda 表达式定义时就已经确定,不会随着外部变量的变化而变化。
值捕获的变量默认不能在 Lambda 表达式中修改,除非使用 mutable 关键字。例如:
int x = 10;
auto f = [x] (int y) -> int { return x + y; }; // 值捕获 x
x = 20; // 修改外部的 x
cout << f(5) << endl; // 输出 15,不受外部 x 的影响
- 引用捕获(capture by reference):在捕获列表中使用 & 加变量名,表示将该变量的引用传递到 Lambda 表达式中,作为一个数据成员。
引用捕获的变量在 Lambda 表达式调用时才确定,会随着外部变量的变化而变化。
引用捕获的变量可以在 Lambda 表达式中修改,但要注意生命周期的问题,避免悬空引用的出现。例如:
int x = 10;
auto f = [&x] (int y) -> int { return x + y; }; // 引用捕获 x
x = 20; // 修改外部的 x
cout << f(5) << endl; // 输出 25,受外部 x 的影响
- 隐式捕获(implicit capture):在捕获列表中使用
=
或&
,表示按值或按引用捕获 Lambda 表达式中使用的所有外部变量。
这种方式可以简化捕获列表的书写,避免过长或遗漏。
隐式捕获可以和显式捕获混合使用,但不能和同类型的显式捕获一起使用。例如:
int x = 10;
int y = 20;
auto f = [=, &y] (int z) -> int { return x + y + z; }; // 隐式按值捕获 x,显式按引用捕获 y
x = 30; // 修改外部的 x
y = 40; // 修改外部的 y
cout << f(5) << endl; // 输出 55,不受外部 x 的影响,受外部 y 的影响
- 初始化捕获(init capture):C++14 引入的一种新的捕获方式,它允许在捕获列表中使用初始化表达式,从而在捕获列表中创建并初始化一个新的变量,而不是捕获一个已存在的变量。
这种方式可以使用 auto 关键字来推导类型,也可以显式指定类型。这种方式可以用来捕获只移动的变量,或者捕获 this 指针的值。例如:
int x = 10;
auto f = [z = x + 5] (int y) -> int { return z + y; }; // 初始化捕获 z,相当于值捕获 x + 5
x = 20; // 修改外部的 x
cout << f(5) << endl; // 输出 20,不受外部 x 的影响
Lambda表达式的优点和缺点
Lambda表达式相比于普通函数和普通类,有以下几个优点:
- 简洁:Lambda表达式可以省略函数名和类名,直接定义和使用,使得代码更加简洁和清晰。
- 灵活:Lambda表达式可以捕获外部变量,可以作为函数参数,也可以作为函数返回值,使得代码更加灵活和方便。
- 安全:Lambda表达式可以控制外部变量的访问方式,可以避免全局变量的定义,可以避免悬空指针和无效引用的产生,使得代码更加安全和稳定。
Lambda表达式的缺点
- Lamdba表达式语法比较灵活,增加了阅读代码的难度
- 对于函数复用无能为力
Lambda表达式的示例
一、使用 Lambda表达式作为函数参数
我们可以使用 Lambda表达式作为函数的参数,这样可以方便地定义和传递一些简单的函数对象,例如自定义排序规则、自定义比较函数等。例如:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 定义一个结构体
struct Item
{
Item(int aa, int bb) : a(aa), b(bb) {}
int a;
int b;
};
int main()
{
vector<Item> vec;
vec.push_back(Item(1, 19));
vec.push_back(Item(10, 3));
vec.push_back(Item(3, 7));
vec.push_back(Item(8, 12));
vec.push_back(Item(2, 1));
// 使用 Lambda表达式,根据 Item 中的成员 a 升序排序
sort(vec.begin(), vec.end(), [] (const Item& v1, const Item& v2) { return v1.a < v2.a; });
// 使用 Lambda表达式,打印 vec 中的 Item 成员
for_each(vec.begin(), vec.end(), [] (const Item& item) { cout << item.a << " " << item.b << endl; });
return 0;
}
二、使用 Lambda表达式作为函数返回值
我们可以使用 Lambda表达式作为函数的返回值,这样可以方便地定义和返回一些简单的函数对象,例如工厂函数、闭包函数等。例如:
#include <iostream>
using namespace std;
// 定义一个函数,返回一个 Lambda表达式,实现两个数的加法
auto make_adder(int x)
{
return [x] (int y) -> int { return x + y; };
}
int main()
{
// 调用函数,得到一个 Lambda表达式
auto add5 = make_adder(5);
// 调用 Lambda表达式
cout << add5(10) << endl; // 输出 15
return 0;
}
Lambda表达式的原理
lambda 表达式会被编译器转换为类(一个匿名类的匿名对象),这个类的名称由编译器自动生成,我们无法直接获取或使用,并在类中重载函数调用运算符,详情见下:
// 编译器处理前代码
#include <iostream>
using namespace std;
int main()
{
int a = 10;
auto addFun = [a](int b) -> int
{ return a + b; };
addFun(5);
return 0;
}
//编译器处理后代码
#include <iostream>
using namespace std;
int main()
{
int a = 10;
// 定义一个匿名类
class __lambda_7_19
{
public:
inline /*constexpr */ int operator()(int b) const // 重载函数调用运算符()
{
return a + b;
}
private:
int a;
public:
__lambda_7_19(int &_a) // 构造函数
: a{_a}
{
}
};
// 创建匿名对象
__lambda_7_19 addFun = __lambda_7_19{a};
addFun.operator()(5);
return 0;
}
小结
lamda 匿名函数的实现原理,就是编译器将会生成一个类,该类的构造函数的参数为 lamda 函数的捕获参数;该类的函数调用操作符函数的参数为 lamda 函数的参数列表;函数调用操作符函数的函数体为 lamda 函数体
C++14 和 C++17 对 Lambda表达式的扩展和改进
- 泛型 Lambda:C++14 允许在 Lambda表达式的参数列表和返回值类型中使用
auto
关键字,从而实现泛型 Lambda,即可以接受任意类型的参数和返回任意类型的值的 Lambda表达式。 - 初始化捕获:C++14 允许在 Lambda表达式的捕获列表中使用初始化表达式,从而实现初始化捕获,即可以在捕获列表中创建和初始化一个新的变量,而不是捕获一个已存在的变量。
- 捕获 this 指针:C++17 允许在 Lambda表达式的捕获列表中使用
*this
,从而实现捕获 this 指针,即可以在 Lambda表达式中访问当前对象的成员变量和成员函数。
#include <iostream>
using namespace std;
// 定义一个类
class Test
{
public:
Test(int n) : num(n) {} // 构造函数,初始化 num
void show() // 成员函数,显示 num
{
cout << num << endl;
}
void add(int x) // 成员函数,增加 num
{
// 定义一个 Lambda表达式,捕获 this 指针
auto f = [*this] () { return num + x; };
// 调用 Lambda表达式
cout << f() << endl;
}
private:
int num; // 成员变量,存储一个整数
};
int main()
{
Test test(10); // 创建一个 Test 对象
test.show(); // 调用成员函数,输出 10
test.add(5); // 调用成员函数,输出 15
return 0;
}