首页 > 编程笔记 > C++笔记 阅读:7

C++ Lambda表达式的用法(非常详细)

Lambda 表达式是一种匿名函数,省略了函数名,只保留了函数体、参数和返回值类型。在 C++ 中,Lambda 表达式正是用来表达匿名函数的理想选择。

尽管函数指针和函数对象在 STL 算法中被广泛用于自定义算法行为,但在某些情况下,它们可能显得不够灵活。

首先,函数或函数对象类的定义与使用地点是分离的,这意味着如果开发者希望在函数使用的地方查看其定义,可能需要在多个代码文件之间来回跳转。这种分离打断了代码的流程,影响了代码的可读性,也降低了开发的效率。

此外,STL 算法中使用的函数数量众多,但它们往往执行非常简单的操作。如果使用普通函数或函数对象来实现这些小函数,会导致程序代码膨胀,使得程序充斥着难以管理和维护的小函数。

为了解决这些问题,C++ 引入了 Lambda 表达式。

Lambda 表达式在功能上与函数指针和函数对象相同,可以方便地应用于 STL 算法中以实现自定义行为。使用 Lambda 表达式时,可以在使用函数的地方直接定义它,使得代码更加流畅自然。

Lambda 表达式还允许直接访问外部作用域中的数据,避免了复杂的参数传递,同时解决了函数执行过程中状态数据的保存问题,极大地方便了数据处理。

在语法上,Lambda 表达式的简洁语法降低了使用难度,减少了因语法复杂可能带来的错误。

前面我们使用函数对象完成了对容器中 Student 对象身高的统计任务。虽然问题得到了解决,但程序的实现略显臃肿,缺乏优雅。通过引入 Lambda 表达式,我们可以简化代码,提高程序的可读性和维护性,让程序穿上更加合身的“衬衣”,展现出更加优雅和高效的编程风格。

下面来看 Lambda 表达式如何让程序恢复“优雅”。以下是使用 Lambda 表达式统计学生身高的示例代码:
// 定义变量,用于保存函数执行过程中的状态数据
int nTotalHeight = 0;
int nCount = 0;
// 在 for_each() 算法中使用 Lambda 表达式,统计身高
for_each( vecStu.begin(), vecStu.end(),
    [] (const Student& st)  // Lambda 表达式
{
        nTotalHeight += st.GetHeight();  // 直接访问外部数据,累计身高
        ++nCount;
    });
// 输出统计结果
if( 0 != nCount )
{
    cout << nCount << "个学生的平均身高是: "
         << (float)nTotalHeight/nCount << endl;
}
短短的几行代码就完成了原来需要一个函数对象类才能完成的工作。

在这段代码中,我们在 for_each() 算法中原来放置函数对象第三个参数的位置,使用一对方括号“[]”来表示 Lambda 表达式的开始,其后的(const Student& st)是这个表达式的参数列表。

因为返回值无关紧要,我们省略了 Lambda 表达式的返回值类型。

当 for_each() 算法循环遍历容器中的每一个 Student 对象时,它会将 Student 对象作为参数传递给 Lambda 表达式,从而将 Student 对象传递到 Lambda 内部。

因为方括号“[]”中包含“&”符号,所以在 Lambda 表达式内部可以以引用的形式直接访问外部的任何变量。我们通过成员函数获得 Student 对象的身高,并直接累加到 nTotalHeight 变量中,实现了函数执行过程中状态数据的保存。

for_each() 算法执行完毕后,nTotalHeight 和 nCount 已经保存了统计数据,接下来只需进行简单的计算并将结果输出,就“优雅”地完成了整个统计工作。

C++ Lambda表达式的定义与使用

在上面的例子中,我们已经见识过了 Lambda 表达式的简洁与便利。具体而言,在 C++ 中定义一个 Lambda 表达式的语法格式如下:
[变量使用说明符号](参数列表) -> 返回值数据类型
{
    // 函数体
}
其中,方括号“[]”表示 Lambda 表达式的开始,用来告诉编译器接下来的代码是一个 Lambda 表达式。

在方括号中,可以指定 Lambda 表达式对当前作用域(也就是 Lambda 表达式所在的花括号“{}”范围)中的变量的捕捉方式,因此这个方括号也可以称为捕捉列表(capture list)。

如果我们只想在 Lambda 表达式内读取当前作用域内的变量,而不想修改这些变量的数值,那么可以在“[]”方括号内书写一个“=”等号,形成“[=]”的形式。或者直接把方括号留空,这样 Lambda 表达式将以只读方式捕捉当前作用域内的所有变量。

也就是说,在 Lambda 表达式内部只能读取外部变量的值,而不能对其进行修改。如果试图修改,将导致编译错误。例如:
vector<int> v = {51, 85, 63, 44, 58};
int nAdd = 10;
// 为容器中小于 60 的分数加上 10 分
for_each(v.begin(), v.end(),
// “[=]”表示以传值的方式使用 lambda 外部的变量
// 因为要修改容器中的数据,所以参数采用引用形式
[=](const int& x)
{
    nAdd = 20;      // 试图修改外部变量会导致编译错误
    if(x < 60)
    {
        x += nAdd;  // 只读访问 nAdd
    }
});
如果想在 Lambda 表达式内对外部变量进行修改,可以使用“[&]”代替“[=]”作为 Lambda 表达式的开始,这表示 Lambda 表达式将以传引用的方式捕捉当前作用域内的变量。这就意味着 Lambda 表达式内部的变量实际上是外部同名变量的引用,因此在 Lambda 表达式中对这些变量的修改将直接影响当前作用域中的变量。

例如:
int nTotal = 0;
for_each(v.begin(), v.end(),
[&](const int& x)  // “[&]”表示以传引用的方式使用 lambda 外部的变量
{
    nTotal += x;    // 修改变量的值
}),
cout << "容器中数据的总和是:" << nTotal << endl;
在 Lambda 表达式内部,我们将容器中的数据累加到 nTotal 变量中。因为 Lambda 表达式以传引用的形式来捕捉外部变量,所以这个累加实际上操作的是 Lambda 表达式外部的局部变量 nTotal。这样,就实现了数据从 Lambda 表达式内部向外部的传递。

如果需要与 Lambda 表达式传递多个数据,并且这些数据的传递方式各不相同,那么可以在方括号“[]”中的第一个位置用“&”作为 Lambda 表达式的默认传递方式,而需要以传值方式进行传递的变量,则可以单独在方括号“[]”中列出。例如,我们希望在统计成绩的同时修正成绩:
int nAdd = 10;
int nTotal = 0;
for_each(v.begin(), v.end(),
[&, nAdd](const int& x)  // 默认采用传引用访问,nAdd 使用传值访问
{
    if (x < 60)
    {
        x += nAdd;        // 传值访问 nAdd,只能读取
    }
    nTotal += x;          // 默认采用传引用访问 nTotal,可以写入
});
cout << "容器中所有数据的总和是" << nTotal << endl;
Lambda 表达式通过使用方括号“[]”来捕获外部变量,紧接着是它的参数列表。参数列表与普通函数的参数列表相似,主要用于接收 STL 算法传递进来的数据。参数的数量由使用的具体算法决定,参数的类型则由容器中存储的数据类型决定。至于参数的传递方式(传值还是传引用),取决于是否需要在算法执行过程中修改容器中的数据。

例如,如果 Lambda 表达式应用于 for_each() 算法,它通常只需要一个参数来接收算法传递的单个数据项。如果容器中存储的是 int 类型的数据,那么 Lambda 表达式的参数类型也应该是 int。

关于参数的传递方式,如果我们的目的是修改容器中的数据,我们可能会选择传引用的方式(例如第一个例子调整容器中的分数);如果我们只是需要读取数据而不进行修改,那么我们可能会选择传值的方式(例如第二个例子统计容器中的分数)。

在定义了参数列表之后,接下来是 Lambda 表达式的返回值类型。在大多数情况下,Lambda 表达式用于对数据进行简单的处理,不需要返回值,这时可以省略返回值类型的定义。如果某些算法需要 Lambda 表达式有返回值,可以在参数列表后使用“->”符号来定义它的返回值类型。

例如,count_if() 算法要求与之配合的函数具有 bool 类型的返回值,以决定当前数据是否需要统计在内。因此,当 Lambda 表达式应用于 count_if() 算法时,就需要定义返回值类型:
// 统计容器中的及格分数
int nPass = count_if(vecScore.begin(), vecScore.end(),
[=](int x) -> bool  // 定义 Lambda 表达式的返回值类型为 bool 类型
{
    // 判断分数是否及格
    return x >= 60;
});
在这个例子中,我们将 Lambda 表达式的返回值定义为 bool 类型。count_if() 算法在执行过程中,会逐个地将容器中的数据传递给 Lambda 表达式进行判断:
count_if() 算法正是根据 Lambda 表达式的返回值来判断当前数据是否应该统计在内。

Lambda 表达式的定义非常灵活,它没有函数名,返回值类型也不是必须的。它的使用也非常方便,可以以多种方式捕获当前作用域的变量,省去了函数调用过程中的数据传递。同时,定义 Lambda 表达式的位置通常就是使用它的位置,实现了“所见即所得”的效果。

因此,Lambda 表达式特别适合用于 STL 算法中,用以表达对数据的简单操作。例如,使用 Lambda 表达式,可以用短短几行代码实现所需的功能,体现了编程的“优雅”。相比之下,如果使用函数指针或函数对象来实现相同的功能,可能需要编写较多的代码,使简单的事情变得复杂。

在那些只需要对数据进行简单处理的场景中,使用 Lambda 表达式是一个合适的选择。

C++定义可以使用Lambda表达式的函数

在上述例子中,我们展示了 Lambda 表达式在 STL 算法中处理数据的能力。鉴于 Lambda 表达式的便捷性,一个自然的问题是:我们是否可以定义自己的函数,以接受 Lambda 表达式作为参数呢?答案是肯定的。

在 C++ 中,我们可以通过使用 STL 中的 function 类模板来实现这一点。

function 是一个通用的多态函数封装器,它可以存储、调用和复制任何可调用对象,包括 Lambda 表达式、函数、函数对象以及 bind 绑定的函数。

当我们使用特定的返回值类型和不同数量的参数类型来特化 function 类模板时,得到的模板类可以代表具有相应签名的函数。然后,我们可以将这个模板类类型用作自定义函数的参数类型。这样,自定义函数就可以接受具有相应返回值和参数的函数指针或函数对象作为实际参数。在函数内部,我们可以调用通过 function 类型参数传递进来的函数,实现对函数功能的自定义。

从本质上讲,Lambda 表达式也是一种可调用对象。既然 function 类型的参数可以接受函数指针和函数对象,它自然也可以接受相应签名的 Lambda 表达式。

例如,在前面的例子中,我们实现了一个 mycount_if() 算法,它可以接受一个函数指针作为参数。下面我们用 function 类模板对它进行改写,使其不仅可以接受函数指针作为参数,还可以接受相应类型的函数对象或 Lambda 表达式为参数:
#include <functional>  // 包含 function 类模板所在的头文件

// 可以接受 Lambda 表达式的 mycount_if() 算法
int mycount_if(const vector<int>& v,  // 需要统计的容器
// 将函数指针类型更换为 function<bool(int)>类型
// 表示它可以接受一个返回值为 bool 类型,同时拥有一个 int 类型参数的
// 函数指针或函数对象,自然也可以是相应类型的 Lambda 表达式
function<bool(int)> is)
{
    // 函数体无须进行任何修改
}

// ...
// 在 mycount_if() 算法中应用 Lambda 表达式
int nPass = mycount_if(vecScore,
// 返回值为 bool 类型,同时拥有一个 int 类型参数的 Lambda 表达式
[=](int x) -> bool
{
    return x >= 60;  // 判断分数是否及格
});
借助 function 类模板,我们也可以定义接受 Lambda 表达式的函数,从而将函数的部分业务逻辑留给函数的使用者来灵活地实现,使之适应更多需求,大大增加了函数的通用性。

总之,如果数据处理过程非常简单,又不需要反复多次使用,则应使用 Lambda 表达式。

相关文章