深入了解 C++ lambda 表达式:用法、特性和最佳实践
文章目录
- 一、引言
- 1、lambda表达式的概念
- 2、lambda表达式在C++中的重要作用
- 3、lambda表达式的基本语法结构
- 二、lambda表达式的核心特性
- 1、捕获列表
- 2、参数列表
- 3、返回类型
- 4、函数体
- 5、multable关键字
- 三、lambda表达式的进阶用法
- 1、lambda表达式与STL算法的结合使用
- 2、lambda表达式与模板的结合使用
- 四、lambda表达式与函数对象的比较
- 1. lambda表达式与函数指针
- 2. lambda表达式与仿函数
一、引言
1、lambda表达式的概念
lambda表达式,起源于数学中的λ演算,是现代编程语言中函数式编程的一个核心概念。在C++中,lambda表达式是一种可以定义匿名函数的语法结构,允许在需要函数作为参数的地方直接定义和传递函数,从而提高了代码的简洁性和可读性。这种特性使得lambda表达式在C++编程中扮演着至关重要的角色。
2、lambda表达式在C++中的重要作用
lambda表达式在C++中的重要作用主要体现在以下几个方面:
首先,lambda表达式可以方便地定义简单的函数功能,如排序、筛选等,使得代码更加紧凑和易于理解。
其次,lambda表达式可以作为参数传递给其他函数,特别是在STL算法中,lambda表达式可以作为谓词(Predicate)使用,实现更加灵活的功能。
此外,lambda表达式还可以与STL容器、智能指针等一起使用,简化代码编写,提高代码的可维护性。
总的来说,lambda表达式的引入使得C++代码更加灵活、简洁,提高了代码的可读性和可维护性。
3、lambda表达式的基本语法结构
一个lambda表达式表示一个可调用的代码单元。我们可以将其理解为一个未命名的内联函数。与任何函数类似,一个lambda具有一个返回类型、一个参数列表和一个函数体。
lambda表达式的基本语法结构如下:
[capture](parameters) mutable -> return_type {body}
-
[capture]
:捕获列表,用于捕获外部作用域的变量,以在lambda函数体内使用。捕获方式可以是值捕获(通过复制)或引用捕获(通过引用)。捕获列表是可选的,但如果需要使用外部变量,则必须提供。 -
(parameters)
:参数列表,用于定义lambda函数的输入参数。参数列表也是可选的,可以定义零个或多个参数。若没有参数,则可以省略括号。 -
mutable
:默认情况下,lambda函数总是一个const函数,mutable
可以取消其常量性。使用该修饰符时,参数列表不可省略(即使为空)。 -
-> return_type
:返回值类型,用于指定lambda函数的返回类型。如果lambda函数没有返回值,可以省略返回类型部分。否则,需要显式指定返回类型。 -
{body}
:lambda函数的函数体,包含了lambda函数的具体实现代码。除了可以使用其形参外,还可以使用所有捕获到的变量。
我们可以忽略参数列表和返回值类型,但必须包含捕获列表和函数体。
auto f = [] { return 1; }
它的调用方式与普通函数相同:
cout << f() << endl;
在lambda中忽略括号和参数列表等价于指定一个空参数列表。在此例中,当调用 f 时,参数列表是空的。如果忽略返回类型,lambda根据函数体中的代码推断出返回类型。如果函数体只是一个return语句,则返回类型从返回的表达式的类型推断而来。否则,返回类型为 void。
????如果 lambda 的函数体包含任何单一 return 语句之外的内容,且未指定返回类型,则返回 void。
lambda表达式不是一个函数,在底层编译器将其转化为仿函数。
二、lambda表达式的核心特性
1、捕获列表
在lambda表达式中,捕获列表用于定义lambda体可以访问哪些外部变量。捕获方式主要有两种:值捕获和引用捕获。
- 值捕获(Value Capture):当变量以值的方式被捕获时,lambda会复制该变量的值到其内部状态中。在lambda体内部对捕获的变量所做的任何修改都不会影响原始变量。
int x = 10;
auto lambda = [x]() { x = 20; }; // 值捕获,lambda内部的x是x的一个副本
lambda();
// x的值仍然是10,lambda内部的修改不影响外部变量
- 引用捕获(Reference Capture):当变量以引用的方式被捕获时,lambda会保存该变量的引用,从而在lambda体内部可以直接访问和修改原始变量。
int x = 10;
auto lambda = [&x]() { x = 20; }; // 引用捕获,lambda内部的x是外部x的引用
lambda();
// x的值现在是20,因为lambda内部修改了引用指向的变量
引用捕获与返回引用有着相同的问题和限制。如果我们采用引用方式捕获一个变量,就必须确保被引用的对象在lambda执行的时候是存在的。
除了值捕获和引用捕获外,捕获列表还有显式捕获和隐式捕获之分。
-
显式捕获(Explicit Capture):在捕获列表中明确列出要捕获的变量,使用
&variable
或variable
来混合使用。
int a = 1, b = 2;
auto lambda = [&a, b]() { /* ... */ }; // a是引用捕获,b是值捕获
-
隐式捕获(Implicit Capture):通过捕获列表的
[&]
或[=]
来隐式指定捕获方式。[&]
表示以引用方式捕获所有外部变量,[=]
表示以值方式捕获所有外部变量。
int c = 3;
auto lambdaImplicitRef = [&]() { c = 4; }; // 隐式引用捕获所有变量
auto lambdaImplicitVal = [=]() { /* c的值不能被修改 */ }; // 隐式值捕获所有变量
-
显式捕获和隐式捕获同时使用:这种情况下,捕获列表的第一个元素必须是
[&]
或[=]
。 -
捕获列表不允许变量重复传递:
[=,a]
或[&,&a]
都是不可以的,导致编译错误。 - 只能捕获当前所在作用域中的局部变量:捕获非此作用域或非局部变量都会导致编译错误。
捕获列表的设计使得lambda表达式能够灵活地访问和操作外部作用域中的变量,同时提供了对变量生命周期的精细控制。然而,使用lambda表达式时需要谨慎处理捕获的变量,以避免不必要的性能开销和潜在的生命周期问题。
一个lambda只有在其捕获列表中捕获一个它所在函数中的局部变量,才能在函数体中使用该变量。
2、参数列表
lambda表达式的参数列表定义了lambda函数所接受的输入参数。这个列表的语法与常规函数的参数列表非常相似,允许你指定参数的类型和名称。
- 无参数lambda
如果lambda表达式不需要任何参数,参数列表可以留空。这样的lambda表达式可以被调用而不传递任何参数。
auto noParamlambda = []() {
std::cout << "This lambda has no parameters." << std::endl;
};
noParamlambda(); // 调用无参数lambda
- 带参lambda
带参数的lambda表达式在参数列表中定义参数,并在lambda体中使用这些参数。参数可以具有任何有效的C++类型,包括基本类型、指针、引用、复杂类型以及自定义类型。
auto addlambda = [](int a, int b) {
return a + b;
};
int sum = addlambda(3, 4); // 调用带参数的lambda,并返回7
std::cout << "Sum: " << sum << std::endl;
在上面的例子中,addlambda
是一个接受两个整数参数a
和b
的lambda表达式,并返回它们的和。当调用addlambda(3, 4)
时,lambda函数执行,并返回结果7。
lambda表达式的参数列表也可以包含默认参数和可变参数模板,就像在常规函数中一样,但这样的用法相对不常见。
3、返回类型
在C++的lambda表达式中,返回类型可以是隐式推断的,也可以显式指定。这取决于lambda表达式的实际使用情况和需求。
- 推断返回类型
如果lambda表达式的体只有一个返回语句,并且该语句的类型是明确的,那么编译器可以自动推断出lambda的返回类型。这种情况下,我们不需要显式指定返回类型。
auto add = [](int a, int b) { return a + b; };
int sum = add(3, 4); // 推断返回类型为int
在这个例子中,lambda表达式有一个返回语句return a + b;
,其返回类型是int
。由于返回类型是明确的,编译器可以自动推断出add
的返回类型为int
。
- 显式指定返回类型
在某些情况下,lambda表达式的返回类型可能不那么明显,或者我们想要明确指定返回类型以提高代码的可读性。这时,我们可以使用尾置返回类型来显式指定lambda的返回类型。尾置返回类型的语法是在参数列表后面使用->
,然后跟上返回类型。
auto func= [](int n) -> int {
if (n == 0) return 1;
else return n * (n - 1);
};
在这个例子中,尽管lambda表达式的返回类型可以从return
语句中推断出来,但我们还是显式指定了返回类型为int
。这样做的好处是使得lambda表达式的意图更加明确,特别是在返回类型不是显而易见的情况下。
需要注意的是,如果lambda表达式没有返回语句,或者返回类型是void
,那么不需要指定返回类型,因为void
是默认的返回类型。
auto print = [](int n) {
std::cout << n << std::endl;
};
在这个例子中,lambda表达式没有返回语句,因此不需要指定返回类型。编译器会默认将其返回类型推断为void
。
4、函数体
在C++中,lambda表达式的函数体可以是单条语句,也可以是多条语句。这取决于lambda表达式的具体需求以及你想要在lambda内部执行的逻辑。
例如,你可以使用lambda表达式来定义一个比较函数,用于在排序算法中比较元素:
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> numbers = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};
// 使用lambda表达式作为比较函数
std::sort(numbers.begin(), numbers.end(), [](int a, int b) {
return a < b;
});
// 输出排序后的数组
for (int num : numbers) {
std::cout << num << ' ';
}
std::cout << std::endl;
return 0;
}
在这个例子中,lambda表达式[](int a, int b) { return a < b; }
被用作std::sort
函数的比较函数。这个lambda接受两个整数参数a
和b
,并返回一个布尔值,指示a
是否小于b
。这使得std::sort
能够根据这个比较函数对numbers
数组进行排序。
5、multable关键字
在C++中,mutable
关键字的用法主要体现在两个方面:一是用于类的成员变量,二是用于lambda表达式。
在类的成员变量中使用:
mutable
关键字主要用于突破const
成员函数的限制,使其能够修改类的某些成员变量。通常情况下,const
成员函数不能修改类的任何成员变量,但是被mutable
修饰的成员变量是个例外。这样的设计使得在需要保持对象整体不被修改的情况下,仍然能够允许某些成员变量被修改。
下面是一个简单的例子:
class MyClass {
private:
mutable int mutable_member;
int non_mutable_member;
public:
MyClass() : mutable_member(0), non_mutable_member(0) {}
void updateMutableMember() const {
mutable_member = 42; // 允许在const成员函数中修改mutable成员的值
}
};
在这个例子中,mutable_member
是一个mutable
成员变量,因此在const
成员函数updateMutableMember
中也可以被修改。而non_mutable_member
则不能在const
成员函数中被修改。
在lambda表达式中使用:
在lambda表达式中,mutable
的作用主要是允许修改通过值捕获的变量。默认情况下,lambda表达式通过值捕获的变量在lambda体内部是常量,不能被修改。但如果在lambda的捕获子句中使用了mutable
关键字,则这些变量可以被修改。
下面是一个lambda表达式中使用mutable
的例子:
int main() {
int count = 0;
auto lambda = [count]() mutable { ++count; }; // 使用mutable允许修改捕获的count变量
lambda();
std::cout << "Count: " << count << std::endl; // 输出:Count: 1
return 0;
}
在这个例子中,count
是通过值捕获到lambda表达式中的。由于使用了mutable
关键字,因此在lambda体内部可以修改count
的值。
需要注意的是,虽然mutable
提供了在const
环境或lambda表达式中修改变量的能力,但这也会增加代码的复杂性和出错的可能性。因此,在使用mutable
时应该谨慎考虑,确保它的使用是合理且必要的。
三、lambda表达式的进阶用法
1、lambda表达式与STL算法的结合使用
lambda表达式与C++标准模板库(STL)算法的结合使用是C++11及以后版本中非常强大的功能之一。STL算法通常需要谓词(即返回布尔值的函数或可调用对象)来定义算法的行为,而lambda表达式提供了灵活的方式来定义这些谓词。
- 使用lambda表达式作为STL算法的谓词
谓词是一个返回布尔值的函数或可调用对象,它通常用于在STL算法中决定元素的条件。例如,在std::remove_if
算法中,我们使用谓词来决定哪些元素应该被移除。
#include <vector>
#include <algorithm>
#include <iostream>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9};
// 使用lambda表达式作为谓词,移除所有偶数
numbers.erase(std::remove_if(numbers.begin(), numbers.end(), [](int num) {
return num % 2 == 0;
}), numbers.end());
// 输出剩余元素
for (int num : numbers) {
std::cout << num << ' ';
}
std::cout << std::endl;
return 0;
}
在这个例子中,lambda表达式[](int num) { return num % 2 == 0; }
被用作std::remove_if
的谓词,用于判断一个数是否是偶数。所有偶数都被移动到numbers
容器的末尾,并通过erase
方法删除。
- lambda表达式在排序、查找等算法中的应用
lambda表达式也可以用于定义排序准则,比如std::sort
中的比较函数。
#include <vector>
#include <algorithm>
#include <iostream>
int main() {
std::vector<std::string> words = {"banana", "apple", "cherry", "date"};
// 使用lambda表达式作为比较函数进行排序
std::sort(words.begin(), words.end(), [](const std::string& a, const std::string& b) {
return a.size() < b.size();
});
// 输出排序后的单词
for (const auto& word : words) {
std::cout << word << ' ';
}
std::cout << std::endl;
return 0;
}
在这个例子中,我们根据字符串的长度对单词进行排序。lambda表达式[](const std::string& a, const std::string& b) { return a.size() < b.size(); }
被用作std::sort
的比较函数。
2、lambda表达式与模板的结合使用
lambda表达式可以与模板函数一起使用,以提供更高的灵活性。模板函数可以接收任何满足特定签名要求的可调用对象,包括lambda表达式。这使得模板函数能够适用于各种情况,而不仅仅是预定义的函数或函数对象。
下面是一个示例,展示了如何在模板函数中使用lambda表达式:
#include <iostream>
#include <vector>
#include <algorithm>
// 模板函数,接受一个容器和一个可调用对象
template <typename Container, typename Callable>
void processContainer(const Container& c, Callable func) {
for (const auto& elem : c) {
func(elem);
}
}
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
// 使用lambda表达式作为参数传递给模板函数
processContainer(numbers, [](int n) { std::cout << n << " "; });
std::cout << std::endl;
// 另一个lambda表达式,这次进行平方运算并打印
processContainer(numbers, [](int n) { std::cout << n * n << " "; });
std::cout << std::endl;
return 0;
}
在这个例子中,processContainer
是一个模板函数,它接受一个容器和一个可调用对象(在本例中是lambda表达式)。它遍历容器中的每个元素,并对每个元素调用提供的可调用对象。通过这种方式,我们可以轻松地改变对容器中元素的处理方式,只需传递不同的lambda表达式即可。
四、lambda表达式与函数对象的比较
1. lambda表达式与函数指针
相似之处:
- 函数指针和lambda表达式都可以作为函数参数传递,或者用于回调机制。
- 两者都代表可调用实体,即它们都可以被调用。
不同之处:
- 类型安全:lambda表达式的类型由编译器根据捕获列表和返回类型自动推断,提供了更强的类型安全。函数指针的类型则是固定的,指向具有特定签名的函数。
- 捕获状态:lambda表达式能够捕获其所在作用域中的局部变量,这使得它们能够访问和操作这些变量的状态。而函数指针只能访问全局变量或作为参数传递的变量。
- 语法简洁性:lambda表达式提供了更简洁的语法,允许在表达式中直接定义函数体,而无需事先声明函数。函数指针则需要指向已定义的函数。
2. lambda表达式与仿函数
相似之处:
- 仿函数(通过重载
operator()
的类对象)和lambda表达式都是可调用对象,即都可以像函数一样被调用。 - 两者都可以携带状态,即它们可以包含成员变量来存储和操作数据。
不同之处:
-
语法简洁性:lambda表达式提供了一种更简洁、更直观的方式来定义短小的可调用对象。相比之下,仿函数需要定义一个完整的类,并重载
operator()
,这在某些情况下可能会更加繁琐。 -
类型安全:lambda表达式的类型由编译器自动推断,提供了类型安全。而仿函数的类型则完全由用户定义,这增加了类型安全的责任。
-
灵活性:lambda表达式在定义时可以直接捕获外部变量,这使得它们能够更灵活地访问和操作上下文中的数据。仿函数则需要通过成员变量或参数来传递和存储数据。