C++之lambda表达式
在学习lambda表达式之前,我们需要认识几个基本概念。
谓词
先用一个常见的sort函数举例,sort函数是一个C++算法库中的函数,用于给排序操作。sort函数接收三个参数,前两个是必选参数,后一个是可选参数。
- 开始迭代器(必需):指向要排序序列开始的迭代器。
- 结束迭代器(必需):指向要排序序列结束的迭代器。注意这个迭代器实际上是指向序列末尾元素的下一个位置,即排序范围是左闭右开的 [first, last)。
- 比较函数(可选):一个可以接受两个序列中元素类型的参数并返回一个布尔值的函数或函数对象。这个函数用于比较两个元素的大小,如果第一个元素应该排在第二个元素之前,则返回 true。如果不提供此参数,则 std::sort 使用元素类型的 < 操作符进行比较。
注意到第三个参数传入的是一个函数。我们将函数作为传入参数称为谓词。
其中谓词又分为一元谓词和二元谓词,几元代表着作为参数的该函数需要传入几个参数。例如上面的sort函数,传入的就是一个二元谓词,因为需要比较两个元素的大小并返回布尔值。
假设我们排序一个几个字符串,排序方式是按照长度进行排列。
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
bool isShorter(std::string s1, std::string s2)
{
return s1.size() < s2.size();
}
int main()
{
std::vector<std::string> words;
words.push_back("hello");
words.push_back("alice");
words.push_back("homework");
words.push_back("flowers");
std::sort(words.begin(), words.end(), isShorter);
for (auto& word : words)
{
std::cout << word << std::endl;
}
return 0;
}
输出为:
hello
alice
flowers
homework
重载函数调用运算符
除了上面直接传入参数名之外,我们可以传入任何类别的可调用对象。所谓可调用对象,就是能够对其使用调用运算符,也就是表达式(参数列表)的形式。
常用的可调用对象有函数,函数指针除此之外,还有重载了函数调用运算符的类和lambda表达式。这次我们先简单介绍第三种可调用对象:重载函数调用运算符的类。
用法
还是上面的字符串从大到小输出,这次将会定义一个类,并把它变为可调用对象。
class IsShorter {
public:
bool operator()(const std::string& s1, const std::string& s2) {
return s1.size() < s2.size();
}
};
这个重载函数接收两个字符串,返回一个bool值。IsShorter可以称为函数对象。
特性
这样的写法看起来很偏很怪,但是也有一定的优势。使用函数对象可能会比使用普通函数或lambda表达式带来性能上的优势,因为编译器可能更容易内联对象的 operator() 方法。
你可以为不同的参数类型和数量重载多个版本的 operator(),从而使得函数对象可以以不同的方式被调用。
函数对象经常在需要回调函数的场景中使用,尤其是在模板编程和使用标准库算法时。
lambda表达式
接下来介绍lambda表达式。
定义
一个lambda表达式可以表示一个可调用的代码单元。可以理解为一个未命名的内联函数。一个lambda有一个返回类型、一个参数列表和一个函数体。一个lambda表达式有如下的形式:
[ capture_clause ] ( parameters ) -> return_type {
function_body
}
- capture_clause: 捕获子句,定义了lambda表达式可以从封闭作用域中捕获哪些变量,以及捕获的方式(值捕获或引用捕获)。
- []:不捕获任何外部变量。
- [x, &y]:值捕获变量 x,引用捕获变量 y。
- [=]:值捕获所有外部变量。
- [&]:引用捕获所有外部变量。
- [=, &x]:值捕获所有外部变量,但是引用捕获变量 x。
- [&, x]:引用捕获所有外部变量,但是值捕获变量 x。
- parameters: 参数列表,与普通函数的参数列表类似。如果不需要参数,则可以省略或写成空括号 ()。
- return_type: 返回类型,与定义函数类似。这部分是可选的,如果省略,编译器会根据函数体自动推导返回类型。
- function_body:函数体。
lambda的调用方式和普通函数的调用方式相同,例如:
auto f = [] { return 42; };
std::cout << f() << std::endl; // 42
lambda作为二元谓词传入sort
下面我们用lambda重写之前的sort。
std::sort(words.begin(), words.end(),
[](const std::string s1, const std::string s2)
{
return s1.size() < s2.size();
});
Lambda表达式允许你在调用函数的地方直接定义行为,这减少了需要定义一个单独的比较函数或函数对象的需要。这使得代码更加简洁,易于阅读和维护。使用lambda表达式,你、不需要为比较函数提供一个名称,这避免了在命名空间中引入额外的名称,特别是当这个比较逻辑只需要一次时。Lambda表达式在C++11及以后的版本中得到了广泛支持,成为C++中编写现代、简洁代码的首选方式。
值捕获
lambda可以捕获当前作用域下的变量,捕获的方式分为值捕获和引用捕获。先来看值捕获。
lambda的值捕获与传值参数类似,采用值捕获的前提是变量可以拷贝。值捕获的变量不能在lambda内部修改。在lambda之外修改被捕获的变量也不会影响lambda下已经捕获的变量。下面是一个例子:
int v1 = 42;
auto f = [v1] { return v1; }; // f已经捕获了v1的值,并且保存了下来。
std::cout << f() << std::endl; // 42
v1 = 44;
std::cout << f() << std::endl; // 42
要捕获作用域内所有的值,可以用[=]。
int v1 = 42;
int v2 = 24;
auto f = [=] { return v1 + v2; }; // f捕获了定义域内所有的值
std::cout << f() << std::endl; // 66
引用捕获
和值捕获不同,引用捕获的参数在后续修改时会影响lambda捕获到的变量。
int v1 = 42;
int v2 = 24;
auto f = [&] { return v1 + v2; }; // f捕获了定义域内所有的引用
auto g = [&v2] { return v2; };
std::cout << f() << std::endl; // 66
v2 = 100;
std::cout << g() << std::endl; // 100
std::cout << f() << std::endl; // 124
lambda也可以修改捕获的引用:
int v1 = 23;
auto f = [&v1] {v1 = 33; return v1; };
f();
std::cout << v1 << std::endl; // 33
但是引用捕获有一个问题。如果在lambda调用的时候,引用捕获的变量已经不复存在,这时候就会引发问题。
int* p;
p = new int(42);
auto f = [&p] { return *p; };
std::cout << f() << std::endl;
delete p;
// std::cout << f() << std::endl; // 报错,p指向的值已经不存在。
因此使用lambda捕获也需要慎重。
当捕获一个普通变量,例如int,string,用简单的值捕获就可以了。当捕获指针,迭代器采用引用捕获时,要保证lambda调用的时候对象仍然存在。一般来说,我们应当尽量减少捕获的数据量,慎用全部捕获,来避免潜在问题。如果可能的话,应当避免捕获指针和引用。
可变lambda
引用捕获可以修改,但是值捕获却不可以修改。那么有没有什么办法能够修改捕获到的值?只需要在参数列表后加上关键字mutabale即可。
int v1 = 23;
auto f = [v1] () mutable { ++v1; return v1; };
v1 = 33;
std::cout << f() << std::endl; // 24
参数绑定
有的泛型算法只支持一元谓词,如果我们想要强行传入二元谓词的lambda表达式应该怎么做?
答案是可以使用C++提供的参数绑定功能。使用bind关键字即可,该关键字在functional头文件中。其基本用法是:
auto newCallable = bind(callable, arg_list);
示例:
auto check_size = [](const string s, const int size)
{
return s.size() >= size;
};
auto check6 = bind(check_size, placeholders::_1, 6);
bool flag1 = check_size("example", 6); // true
bool flag2 = check6("example"); // true
在上面的实例中,我们将check_size的第二个参数和6进行了绑定,生成了新的调用函数。通过这样,实现了只有一个谓词的要求。
在使用bind时,我们使用了placeholders来占位,这个命名空间在std中。通过下划线和数字的占位符,我们能够决定参数的传入顺序,像是这样:
auto check_size = [](const string s, const int size)
{
return s.size() >= size;
};
auto size_check = bind(check_size, placeholders::_2, placeholders::_1);
bool flag1 = check_size("example", 6); // true
bool flag2 = size_check(6, "example"); // true
评论区