Lambda表达式是一种匿名函数,允许我们在不声明方法的情况下,直接定义函数。它是函数式编程的一种重要特性,常用于简化代码、优化程序结构和增强代码可读性。
lambda表达式的语法非常简单,具体定义如下:
[ captures ] ( params ) specifiers exception -> ret { body }
举例:
#include <iostream>
int main()
{
int x = 3, y = 4;
auto res = [x](int y)->int{
return x + y;
};
std::cout << res(y) << std::endl;
system("pause");
return 0;
}
- [ captures ] —— 捕获列表,它可以捕获当前函数作用域的零个或多个变量,变量之间用逗号分隔。在对应的例子中,[x]是一个捕获列表,不过它只捕获了当前函数作用域的一个变量x,在捕获了变量之后,我们可以在lambda表达式函数体内使用这个变量,比如return x * y。另外,捕获列表的捕获方式有两种:按值捕获和引用捕获
- ( params ) —— 可选参数列表,语法和普通函数的参数列表一样,在不需要参数的时候可以忽略参数列表。对应例子中的(int y)。
- specifiers —— 可选限定符,C++11中可以用mutable,它允许我们在lambda表达式函数体内改变按值捕获的变量,或者调用非const的成员函数。上面的例子中没有使用说明符。
- exception —— 可选异常说明符,我们可以使用noexcept来指明lambda是否会抛出异常。对应的例子中没有使用异常说明符。
- ret —— 可选返回值类型,不同于普通函数,lambda表达式使用返回类型后置的语法来表示返回类型,如果没有返回值(void类型),可以忽略包括->在内的整个部分。另外,我们也可以在有返回值的情况下不指定返回类型,这时编译器会为我们推导出一个返回类型。对应到上面的例子是->int。
- { body } —— lambda表达式的函数体,这个部分和普通函数的函数体一样。对应例子中的{ return x + y; }。
在lambda函数的定义中,参数列表和返回类型都是可选的部分,而捕捉列表和函数体都可能为空。那么在极端情况下,C++11中最为简略的lambda函数只需要声明为
[]{};
就可以了。不过理所应当地,该lambda函数不能做任何事情。
捕获列表的作用域
捕获列表中的变量存在于两个作用域——lambda表达式定义的函数作用域以及lambda表达式函数体的作用域。
前者是为了捕获变量,后者是为了使用变量。另外,标准还规定能捕获的变量必须是一个自动存储类型。简单来说就是非静态的局部变量。
可以看到,当我们把全局变量和一个静态变量放到捕获列表中会报错。
如果我们要在函数体内使用全局变量和静态变量的话直接使用即可。
#include <iostream>
int x = 0;
int main()
{
static int y = 2;
auto res = []()->int{
return x + y;
};
std::cout << res() << std::endl;
system("pause");
return 0;
}
如果我们将一个lambda表达式定义在全局作用域,那么lambda表达式的捕获列表必须为空。因为根据上面提到的规则,捕获列表的变量必须是一个自动存储类型,但是全局作用域并没有这样的类型,比如:
#include <iostream>
int x = 0;
static int y = 2;
auto res = []()->int {
return x + y;
};
int main()
{
std::cout << res() << std::endl;
system("pause");
return 0;
}
捕获列表的捕获值和引用
捕获列表的捕获方式分为捕获值和捕获引用。
捕获值
#include <iostream>
int main()
{
int x = 3, y = 2;
auto res = [x, y]()->int {
return x + y;
};
std::cout << res() << std::endl; // 5
system("pause");
return 0;
}
捕获引用
捕获引用的语法与捕获值只有一个&的区别,要表达捕获引用我们只需要在捕获变量之前加上&,类似于取变量指针。只不过这里捕获的是引用而不是指针,在lambda表达式内可以直接使用变量名访问变量而不需解引用
#include <iostream>
int main()
{
int x = 3, y = 2;
auto res = [&x, &y]()->int {
return x + y;
};
std::cout << res() << std::endl;
system("pause");
return 0;
}
如果只是读捕获列表中的值,那么上述两个例子没有区别,但是要修改捕获列表的值就有区别。
void bar1()
{
int x = 5, y = 8;
auto foo = [x, y] {
x += 1; // 编译失败,无法改变捕获变量的值
y += 2; // 编译失败,无法改变捕获变量的值
return x * y;
};
std::cout << foo() << std::endl;
}
void bar2()
{
int x = 5, y = 8;
auto foo = [&x, &y] {
x += 1;
y += 2;
return x * y;
};
std::cout << foo() << std::endl;
}
在上面的代码中函数bar1无法通过编译,原因是我们无法改变捕获变量的值。这就引出了lambda表达式的一个特性:捕获的变量默认为常量,或者说lambda是一个常量函数(类似于常量成员函数)。
bar2函数里的lambda表达式能够顺利地通过编译,虽然其函数体内也有改变变量x和y的行为。这是因为捕获的变量默认为常量指的是变量本身,当变量按值捕获的时候,变量本身就是值,所以改变值就会发生错误。相反,在捕获引用的情况下,捕获变量实际上是一个引用,我们在函数体内改变的并不是引用本身,而是引用的值,所以并没有被编译器拒绝。
另外,使用mutable说明符可以移除lambda函数的常量性。例如:
void bar1()
{
int x = 5, y = 8;
auto foo = [x, y]() mutable{
x += 1;
y += 2;
return x * y;
};
std::cout << foo() << std::endl;
}
但是这并不意味着二者就没有任何区别了,捕获值和捕获引用最根本上和函数参数的值传递和引用传递是一样的。例如:
void bar1()
{
int x = 5, y = 8;
auto foo = [x, y]() mutable{
x += 1; // 编译失败,无法改变捕获变量的值
y += 2; // 编译失败,无法改变捕获变量的值
return x * y;
};
std::cout << foo() << std::endl; // 60
std::cout << x << std::endl; // 5
std::cout << y << std::endl; // 8
}
void bar2()
{
int x = 5, y = 8;
auto foo = [&x, &y] {
x += 1;
y += 2;
return x * y;
};
std::cout << foo() << std::endl; // 60
std::cout << x << std::endl; // 6
std::cout << y << std::endl; // 10
}
对于捕获值的lambda表达式还有一点需要注意,捕获值的变量在定义lambda表达式时就固定下来,无论在定义lambda表达式定义之后如何修改这个变量的值,在lambda函数体内都不会改变。例如:
void bar2()
{
int x = 5, y = 8;
auto foo = [x, &y] {
return x * y;
};
x += 1;
y += 2;
std::cout << foo() << std::endl; // 50
std::cout << x << std::endl; // 6
std::cout << y << std::endl; // 10
}
特殊的捕获方法
lambda表达式的捕获列表除了指定捕获变量之外还有3种特殊的捕获方法。
1.[this] —— 捕获this指针,捕获this指针可以让我们使用this类型的成员变量和函数。
class A
{
public:
void Print()
{
std::cout << "class A" << std::endl;
}
void Test()
{
auto foo = [this] {
Print();
x += 2;
std::cout << x << std::endl;
};
foo();
}
private:
int x = 2;
};
2.[=] —— 捕获lambda表达式定义作用域的全部变量的值,包括this。
void bar2()
{
int x = 5, y = 8;
auto foo = [=] {
return x * y;
};
std::cout << foo() << std::endl; // 40
}
3.[&] —— 捕获lambda表达式定义作用域的全部变量的引用,包括this。
void bar2()
{
int x = 5, y = 8;
auto foo = [&] {
return x * y;
};
std::cout << foo() << std::endl; // 40
}
无状态的lambda表达式
C++标准对于无状态的lambda表达式有着特殊的照顾,即它可以隐式转换为函数指针,例如:
void f(void(*)()) {}
void g() { f([] {}); } // 编译成功
在上面的代码中,lambda表达式[] {}隐式转换为void(*)()类型的函数指针。同样,看下面的代码:
void f(void(&)()) {}
void g() { f(*[] {}); }
这段代码也可以顺利地通过编译。我们经常会在STL的代码中遇到lambda表达式的这种应用。
在STL中使用lambda表达式
使用lambda表达式作为STL算法的第三个参数
#include <iostream>
#include <vector>
#include <algorithm>
int main()
{
std::vector<int> x = { 1,2,3,4,5,6 };
std::cout << *std::find_if(x.begin(), x.end(), [](int i) {
return (i % 3) == 0;
}) << std::endl;
system("pause");
return 0;
}
这段代码使用了std::find_if算法来搜索满足指定条件的元素,并输出该元素的值。
首先,创建了一个名为x的std::vector,包含了整数1到6的元素。
然后,调用std::find_if算法,传入参数x.begin()和x.end(),用于指定搜索范围为整个容器x。
在这里,使用了Lambda表达式作为第三个参数,对每个元素进行判断。Lambda表达式[](int i) { return (i % 3) == 0; }的作用是判断一个整数是否能被3整除,如果能则返回true,否则返回false。Lambda表达式的参数为当前迭代到的元素,这里表示为i。
std::find_if算法会依次遍历容器中的元素,并将每个元素传入Lambda表达式中进行判断,直到找到第一个满足条件的元素或遍历完整个容器。一旦找到满足条件的元素,std::find_if算法会返回指向该元素的迭代器。
最后,通过*运算符取出迭代器指向的元素值,并使用std::cout输出到标准输出流中。
因此,这段代码输出的结果是容器x中能被3整除的第一个元素的值,即3。
Lambda 表达式具有以下优点:
-
简洁:Lambda 表达式可以简洁地定义和使用,省去了传统函数的繁琐定义过程。
-
表达力强:Lambda 表达式可以更直观地表达函数的意图,特别适用于一些简单的函数功能。
-
更好的可读性:Lambda 表达式的内联定义使得代码更紧凑,更易于理解和维护。
-
捕获外部变量:Lambda 表达式可以捕获外部作用域的变量,使得在函数体内可以访问这些变量,方便在函数内部进行计算和操作。
然而,Lambda 表达式也有一些缺点:
-
可读性可能有限:过于复杂或嵌套的 Lambda 表达式可能会降低代码的可读性,特别是当 Lambda 表达式较长时。
-
不易重用:Lambda 表达式通常用于一次性的函数功能,不太适用于需要重复使用的情况。对于需要在多个地方使用的函数,使用具名的函数对象或函数指针更具可重用性。
-
难以调试:由于是匿名函数,Lambda 表达式的调试可能相对困难,特别是在复杂的代码场景中。
综上所述,Lambda 表达式在简洁、表达力等方面具有一些优点,但也需谨慎使用,根据具体情况来判断是否适合使用 Lambda 表达式。