lambda简介和使用

前言

在很多编程语言中,lambda非常常见,比如我之前熟悉的Kotlin语言,就把lambda和函数看成一等公民,在源码和官方demo中都有大量使用。这就造成了两个极端,如果不熟悉lambda的话,看这种代码就是煎熬,但是如果熟悉lambda以及其本质的话,lambda的使用可以大大提高可读性。

而在C++中,lambda使用的场景还不是非常多,本篇文章就来简单看一下日常使用,后续文章继续更新lambda深层次的本质原理。

正文

我们以最常见的排序算法来看,一步一步介绍为什么有lambda,以及简单使用。

谓词(predicate)

我们先来看一个简单的排序代码,调用默认的sort函数:

C++ 复制代码
#include <iostream>
#include <algorithm>
#include <vector>
#include <string>

int main() {
        std::vector<std::string> words = {"my", "name", "is", "jack", "six", "years"};
        std::cout << "words:" << std::endl;
        for (auto const &word : words) {
                std::cout << word << std::endl;
        }
        //默认排序
        sort(words.begin(), words.end());
        std::cout << "default sort:" << std::endl;
        for (auto const &word : words) {
                std::cout << word << std::endl;
        }
}

在这个例子中,调用sort方法进行排序,默认使用的是字典序 ,即按照单词首字母进行排序。这时想实现按照其他规则进行排序,比如按照单词的长度进行排序,这时可以给sort传递第三个参数,这个参数就是比较条件,同时这个参数有一个专业术语,叫做谓词(predicate)

谓词是一个可调用的表达式,其返回结果是一个能用作条件的值 。标准库算法所使用的谓词分为2类:一元谓词(unary predicate)和二元谓词(binary predicate),也就是接受单一和两个参数的区别。接受谓词参数的算法,对输入序列中的元素调用谓词 ,所以元素类型必须能够转换为谓词的参数类型

比如下面代码,我们定义一个isShorter方法,就可以替换默认sort版本中的比较规则,代码如下:

C++ 复制代码
bool isShorter(const std::string &s1, const std::string &s2) {
        return s1.size() < s2.size();
}

int main() {
        std::vector<std::string> words = {"my", "name", "is", "jack", "six", "years"};
        std::cout << "words:" << std::endl;
        for (auto const &word : words) {
                std::cout << word << std::endl;
        }
        //默认排序
        sort(words.begin(), words.end());
        std::cout << "default sort:" << std::endl;
        for (auto const &word : words) {
                std::cout << word << std::endl;
        }
        //使用isShorter作为谓词排序
        sort(words.begin(), words.end(), isShorter);
        std::cout << "isShorter sort:" << std::endl;
        for (auto const &word : words) {
                std::cout << word << std::endl;
        }

}

在这种情况下就可以利用新的规则,进行字符串长短排序。这里定义了一个isShorter函数,接受2个参数,进行字符串长度比较,这就是一个二元谓词。然后将之作为参数传递给sort方法,sort方法能正常运行的前提是words中的元素可以转换为std::string类型,并且对每个元素都调用谓词。从语法来看,这里很像是传入一个函数指针。

lambda表达式

根据算法接受一元谓词还是二元谓词,我们传递给算法的谓词必须严格接受一个或者两个参数 ,比如上面的isShorter方法就必须是两个参数。但是,有时我们期望能操作更多的参数。比如现在加个需求:求大于等于一个给定长度的单词有多少。

可以先想一下如何实现,如果只是遍历集和,当然可以,但是我们这里利用先前的知识,可以先把vector排序,然后找到第一个大于或者等于给定长度的字符串,这样后面的字符串都是符合的。伪代码思路如下:

C++ 复制代码
void biggies(std::vector<std::string> &words, 
                std::vector<std::string>::size_type sz) {
        sort(words.begin(), words.end(), isShorter);
        std::cout << "使用isShorter谓词排序后:" << std::endl;
        for (auto const& word : words) {
                std::cout << word << std::endl;
        }
        // 获取一个迭代器,指向第一个满足size() >= sz的元素
        // 计算满足size() >= sz的元素的数目
        // 打印长度大于给定值的单词
}

现在我们的问题就是找到第一个符合条件的元素即可,我们可以使用标准库的find_if算法来查找第一个具有特定大小的元素。

find_if算法接受一对迭代器,表示一个范围,同时第三个参数是一个谓词。find_if算法对输入序列中的每个元素都调用给定的谓词,返回第一个使得谓词返回非0值的元素,如果不存在,返回尾迭代器。

类似前面的sort用法,我们可以编写一个函数,把它当做谓词,代码如下:

C++ 复制代码
bool shorterSz(const std::string &s, 
                std::vector<std::string>::size_type sz) {
        return s.size() >= sz; 
}

可惜的是,这个谓词并不能传递给find_if,因为find_if接受一元谓词,我们传递给find_if的函数必须严格接受一个参数。

lambda简介

在这种情况下,我们可以使用lambda来解决这种情况。我们可以向一个算法传递任何类别的可调用对象(callable object),对于一个对象或者一个表达式,如果可以对其调用运算符(),则可以称为可调用的。在C++中,函数和函数指针肯定是可调用对象,其次就是重载了函数调用运算符的类以及本章将要说的lambda表达式。

一个lambda表达式表示一个可调用的代码单元 ,可以理解为一个未命名的内联函数 。与普通函数类似,一个lambda也具有返回类型、参数列表和函数体 。与普通函数不同的点是lambda可以定义在函数内部

标准的lambda表达形式如下形式:

[caputre list](parameter list) -> return type { function body}

我们给拆开来看:

  • capture list为捕获列表,啥意思呢?前面说了,lambda可以定义在函数中,而lambda同时可以使用所在函数中定义的局部变量,想使用哪些变量,是值拷贝还是引用,这个就由捕获列表来完成。
  • parameter listreturn typefunction body就和普通函数一样,区别就是lambda必须使用尾置返回,即返回类型定义在参数列表之后。

上面基础概念非常重要,对于lambda可以忽略参数列表和返回类型,这时就是无参函数,类型可以自动推导,比如下面代码:

C++ 复制代码
auto f = [] { return 42; };

这里的f是一个可调用对象,我们可以使用调用运算符来调用:

C++ 复制代码
cout << f();

通过这个简单的例子可以看出,lambda的本质是可调用对象,定义来看像是函数的缩写,但是比普通函数多一个捕获列表

向lambda传递参数

既然类似普通函数,所以调用一个lambda时,必须给定实参来初始化lambda的形参。注意点是,lambda不能有默认参数,一旦形参初始化完毕,就可以执行函数体了。

回顾前面的isShorter方法,我们可以定义一个lambda表达式来完成一样的工作:

C++ 复制代码
auto f = [] (const std::string &s1, const std::string &s2) { return s1.size() < s2.size(); };
sort(words.begin(), words.end(), f);
for (auto const& word : words) {
    std::cout << word << std::endl;
}

我们把f当做谓词传递给sort时,当sort需要比较元素时,就会调用f这个lambda表达式,和前面调用isShorter方法一样,会传递参数给lambda表达式。

使用捕获列表

现在我们可以解决前面所说的find_if的问题了,我们想对find_if传入如下函数:

C++ 复制代码
bool shorterSz(const std::string &s, 
                std::vector<std::string>::size_type sz) {
        return s.size() >= sz; 
}

经过发现我们可知,这里的sz其实是方法biggies的局部变量,我们再来看一下该方法:

C++ 复制代码
void biggies(std::vector<std::string> &words, 
                std::vector<std::string>::size_type sz) {
        sort(words.begin(), words.end(), isShorter);
        std::cout << "使用isShorter谓词排序后:" << std::endl;
        for (auto const& word : words) {
                std::cout << word << std::endl;
        }
        // 获取一个迭代器,指向第一个满足size() >= sz的元素
        // 计算满足size() >= sz的元素的数目
        // 打印长度大于给定值的单词
}

这样我们就可以使用lambda表达式来替代原来我们想实现的函数 ,以及使用捕获来获取biggies中的局部变量,这样就可以达到目的:即可以使用外面方法的局部变量,又定义了一个一元谓词,话不多说,直接看代码:

C++ 复制代码
void biggies(std::vector<std::string> &words, 
                std::vector<std::string>::size_type sz) {
        auto f = [] (const std::string &s1, const std::string &s2) { return s1.size() < s2.size(); };
        sort(words.begin(), words.end(), f); 
        std::cout << "使用isShorter谓词排序后:" << std::endl;
        for (auto const& word : words) {
                std::cout << word << std::endl;
        }
        // 获取一个迭代器,指向第一个满足size() >= sz的元素
        auto wc = find_if(words.begin(), words.end(), [sz](const std::string &s){ 
                                return s.size() >= sz; 
                        });
        // 计算满足size() >= sz的元素的数目
        // 打印长度大于给定值的单词
        std::cout << "长度大于" << sz << "的字符串:";
        for_each(wc, words.end(), [](const std::string &s){
                                std::cout << s << " ";
                        });
}

int main() {
        std::vector<std::string> words = {"fox", "jumps", "over", "quick", "red", "slow", "the", "turtle"};
        biggies(words, 4); 
}

//输出
使用isShorter谓词排序后:
fox
red
the
over
slow
jumps
quick
turtle
长度大于4的字符串:over slow jumps quick turtle

在上面代码中,我们传递给find_if的是一个lambda表达式,它的参数只有一个,符合一元谓词的定义。同时又捕获了外部函数中的sz变量,从而达到我们想要的结果。

最后使用for_each函数来打印符合的字符串,同样思考一下,for_each的作用是对每一个集和中的元素进行处理,所以它接收一个一元谓词,参数类型是集和元素类型,所以我们可以使用lambda来替代这个匿名函数。

总结

本篇文章简单介绍了lambda,总结如下:

  • 谓词是一个非常重要的概念,在所有语言和集和相关的API中都会很常见,谓词是一个可调用的表达式,返回结果是可以用作条件判定的值,对于标准库中的算法来说,会把每个元素当成参数来调用这个谓词。
  • 标准库算法中,比如sortfind_if等,我们要明确算法的作用,这样才能理解和写出符合算法的谓词,比如参数类型与个数的区别。
  • lambda也是一个可调用对象,现在阶段可以看成是一个未命名的函数,我们写的方法都可以转成lambda格式。
  • lambda比普通函数更便捷的点是,它可以捕获外部函数的局部变量,这样就完美解决了谓词参数个数的限制问题了。

后续文章,我们详细来探究lambda的本质是什么,捕获的方式有没有其他形式等。

相关推荐
FL162386312916 分钟前
[C++]使用纯opencv部署yolov12目标检测onnx模型
c++·opencv·yolo
JenKinJia22 分钟前
Windows10配置C++版本的Kafka,并进行发布和订阅测试
开发语言·c++
wen__xvn1 小时前
每日一题洛谷P1914 小书童——凯撒密码c++
数据结构·c++·算法
坚定信念,勇往无前1 小时前
springboot单机支持1w并发,需要做哪些优化
java·spring boot·后端
后端小肥肠1 小时前
【AI编程】Java程序员如何用Cursor 3小时搞定CAS单点登录前端集成
前端·后端·cursor
老友@1 小时前
OnlyOffice:前端编辑器与后端API实现高效办公
前端·后端·websocket·编辑器·onlyoffice
云中飞鸿2 小时前
MFC中CString的Format、与XML中的XML_SETTEXT格式化注意
xml·c++·mfc
小小小白的编程日记2 小时前
List的基本功能(1)
数据结构·c++·算法·stl·list
风月歌2 小时前
基于springboot校园健康系统的设计与实现(源码+文档)
java·spring boot·后端·mysql·毕业设计·mybatis·源码
m0_748239473 小时前
Spring Boot框架知识总结(超详细)
java·spring boot·后端