函数对象与lambda

前言

前一篇文章:# lambda简介和使用

在前面的文章中,我们对lambda的定义以及使用有了一个非常清晰的认识,它是一个可调用的对象,定义很像函数指针,包括参数、返回值和函数体3大要素,不同之处是lambda可以定义在函数内,可以通过捕获来获取外部函数中的局部变量。

同时,我们结合sortfind_iffor_each与lambda的使用,更可以加深理解谓词的含义,以及lambda的捕获功能所带来的便捷性,可以在不传递参数的情况下,使用外部变量。

这个非常好用的lambda究竟是什么呢?编程没有魔法,本篇文章来重点探究一下。

正文

想要完全理解lambda,我们不能把lambda看成一个匿名函数,至于为什么,我们可以简单想一下为什么会把lambda看成函数。

lambda定义有参数列表、返回值和函数体,这完全就是一个函数的定义,但是没有名字,所以我们可以看成是匿名函数。同时,在函数体内部,可以使用外部的局部变量,有一种可以实现,那就是内联,相当于lambda代码块是函数的子作用域,所以我们可以看成是匿名的内联函数。

但是有一点不好解释,那就是捕获的对象,lambda可以对捕获的对象进行值捕获(拷贝)、引用捕获,单纯是函数的话,非常不好实现。所以当定义一个lambda时,编译器生成一个与lambda对应的类类型,当给函数传递一个lambda对象时,传递的参数本质是该类类型的未命名对象

至于该类类型结构是什么样子的,如何调用,我们一步一步来分析。

函数调用运算符

如果类重载了函数调用运算符 ,则我们可以像使用函数一样使用该类的对象 。同时因为类可以存储状态,所以比普通函数更加灵活。这两句话需要完全理解,举个例子:

c++ 复制代码
#include <iostream>

struct absInt {
        int operator()(int val) const {
                return val < 0 ? -val : val;
        }
};

int main() {
        int i = -42;
        absInt absObj;
        int absi = absObj(i);
        std::cout << absi << std::endl;
}

在上面代码中,使用operator重载了函数调用运算符() ,它负责接受一个int类型的实参,然后返回绝对值。在absObj(i)的使用中,我们可以调用该对象,就像absObj是一个函数一样。

如果类定义了调用运算符,则把该类的对象称为函数对象(function object),原因非常简单,因为可以调用这种对象,也可以说这种对象的行为像函数一样。

含有状态的函数对象类

搞清楚了重载函数运算符,我们现在探究前面说的它可以做到行为类似函数,但是比函数更灵活的特性。

因为重载运算符的函数必须是类的成员函数,而类的成员函数可以调用其他成员,所以给调用该重载运算符函数提供了更多的操作。我们直接来看个例子:

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

using namespace std;

class PrintString {
        public:
                PrintString(ostream &o = cout, char c = ' '):
                        os(o), sep(c) { }
                void operator() (const string &s) const { os << s << sep; }
        private:
                ostream &os;
                char sep;
};

int main() {
        PrintString printer;
        printer("hello");
        PrintString errors(cerr, '\n');
        errors("error");
}

在代码中,我们定义了一个用于自定义打印字符串的PrintString类,默认使用std::cout和空格符号来打印字符串,同时重载了调用运算符,传入一个待打印的字符串。

在使用中,对于对象printer,可以直接把它当作函数来使用,而对于对象errors,将会使用std::cerr打印,并且后面跟一个换行符。

可以看到,当构建不同的对象时,调用它的效果是不一样的,这就是所体现的灵活性

平时对于这种需求,我们一般都是习惯用函数重载来实现,现在使用重载运算符的方式,可以说是更加灵活。

lambda是函数对象

铺垫了这么多,终于讲到了重点,当我们编写一个lambda时,编译器会将该表达式翻译成一个未命名类的未命名对象。比如上一篇文章中的排序例子:

C++ 复制代码
sort(words.begin(), words.end(), [](const string &a, const string &b){
    return a.size() < b.size();
});

其行为就类似于下面类的一个对象:

C++ 复制代码
class ShorterString {
public: 
    bool operator() (const string &a, const string &b) const {
        return a.size() < b.size();
    }
}

产生的类,只有一个重载了的函数调用运算符成员,参数和lambda完全一样。上一篇文章我们说过,标准库中传递给算法的谓词是一个可调用对象 ,所以前面我们演示了可以传递给sort分别为函数和lambda,现在我们可以传递给它一个类对象

C++ 复制代码
sort(words.begin(), words.end(), ShorterString());

sort内部的代码中,每次比较两个string时,都会调用这个对象。

表示捕获行为的类

既然搞清楚lambda的本质其实就是函数对象后,捕获特性就非常容易理解了。当一个lambda表达式通过引用捕获变量时,将由程序负责确保lambda执行时的对象确实存在,因此,编译器可以直接使用该引用而无需在lambda产生的类中将其存储为数据成员。

相反,通过值捕获的变量会被拷贝到lambda中,这种lambda产生的类必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数,令其使用捕获的变量的值来初始化数据成员。

还是上一篇文章中的例子,我们写了一个lambda传递给find_if用来查询第一个满足条件的元素:

C++ 复制代码
auto wc = find_if(words.begin(), words.end(), [sz](const std::string &s){ 
    return s.size() >= sz; 
});

该lambda使用了值捕获,所以它产生的类将如下:

C++ 复制代码
class SizeComp {
public:
    SizeComp(size_t n): sz(n) { }
    bool operator() (const string &s) const {
        return s.size() >= sz;
    }
private:
    size_t sz;
}

在该类中,直接多了构造函数和用来保存捕获的值的数据成员,上面lambda的写法就和下面效果一样:

C++ 复制代码
auto wc = find_if(words.begin(), words.end(), SizeComp(sz));

这么一看,我们就可以发现lambda其实并没有神奇之处,关于捕获的最特别特性在了解其本质后,也非常容易理解。

总结

本篇文章以重载函数调用运算符开始,逐步揭开lambda的本质,可以看出lambda其实就是一个语法糖,是函数对象的简写方式而已,由编译器为我们做了后续的工作。

当我们能在大脑中为每个lambda都可以自动转换为一个相应的未命名类时,其中的捕获和参数列表也就容易理解,我们使用lambda和阅读lambda将不再困难。

相关推荐
凡人的AI工具箱19 分钟前
15分钟学 Go 第 60 天 :综合项目展示 - 构建微服务电商平台(完整示例25000字)
开发语言·后端·微服务·架构·golang
做人不要太理性23 分钟前
【C++】深入哈希表核心:从改造到封装,解锁 unordered_set 与 unordered_map 的终极奥义!
c++·哈希算法·散列表·unordered_map·unordered_set
java亮小白199727 分钟前
Spring循环依赖如何解决的?
java·后端·spring
程序员-King.31 分钟前
2、桥接模式
c++·桥接模式
chnming198735 分钟前
STL关联式容器之map
开发语言·c++
2301_8112743144 分钟前
大数据基于Spring Boot的化妆品推荐系统的设计与实现
大数据·spring boot·后端
程序伍六七1 小时前
day16
开发语言·c++
小陈phd1 小时前
Vscode LinuxC++环境配置
linux·c++·vscode
火山口车神丶1 小时前
某车企ASW面试笔试题
c++·matlab
草莓base1 小时前
【手写一个spring】spring源码的简单实现--容器启动
java·后端·spring