【C++11】C++11新增语法 Lambda表达式/Lambda的底层原理

article/2023/9/24 22:22:14

Lambda表达式

  • 1 Lambda使用的一个例子
  • 2 Lambda 表达式的语法
  • 3 初次体验Lambda表达式
  • 4 Lambda函数底层实现原理

1 Lambda使用的一个例子

在C++98中,如果我们想要对一个自定义类型进行排序,就需要用户自定义去书写比较的规则。

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Goods
{
public:Goods(string name, double price, int evaluate):_name(name), _price(price), _evaluate(evaluate){}string _name;//名字double _price;//价格int _evaluate;//评价
};
struct ComparePriceLess
{bool operator()(const Goods& g1, const Goods& g2){return g1._price < g2._price;}
};
struct ComparePriceGreater
{bool operator()(const Goods& g1, const Goods& g2){return g1._price > g2._price;}
};
int main()
{vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,3 }, { "菠萝", 1.5, 4 } };sort(v.begin(), v.end(), ComparePriceGreater());//按价格降序排列cout << "降序排列:" << endl;for (int i = 0; i < v.size(); i++)//打印结果{cout << v[i]._name << v[i]._price << " " <<  v[i]._evaluate << endl;}cout << endl;cout << "升序排列:" << endl;sort(v.begin(), v.end(), ComparePriceLess());//按价格升序排列for (int i = 0; i < v.size(); i++)//打印结果{cout << v[i]._name << v[i]._price << " " << v[i]._evaluate << endl;}return 0;
}

在这里插入图片描述

随着C++的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法就要去重新写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,C++11语法中新增了Lambda表达式。

//按价格降序排序
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._price > g2._price; })
//按价格升序排序
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._price < g2._price; })

上述的代码就是Lambda表达式来解决,可以看出Lambda表达式实际上是一个匿名函数。


2 Lambda 表达式的语法

[capture-list] (parameters) opt -> return-type {statement}

[cpature] 为捕获列表(不捕获时,[]不能省略)

①[]、[&]和[=]分别表示不捕获、按引用捕获、按值捕获所有父作用域中内的局部变量。(父作用域指包含lambda表达式的语句块,如main函数)。

◆lambda函数只能捕获父作用域中的局部变量,而捕获非父作用域或static变量都会报错(不是C++11的标准,其行为可能因编译器而不同)。(注意全局变量或static变量不能被捕获。即不能被写入捕获列表中,但可在lambda的函数体内直接访问)

◆默认下无法修改按值捕获的外部变量(因为lambda表达式的operator()默认是const函数,但捕获this指针后可修改成员变量的值,因为是通过this指针来修改的)。(涉及到lambda的底层原理,后面会讲)

◆在类中如果使用&或=捕获,会同时默认捕获this指针。

②[=,&foo]按值捕获外部作用域中所有变量,并按引用捕获foo变量。注意,捕获列表不允许变量重复传递,如[=,var],var被按值捕获了两次,这是不允许的。

③[bar]按值捕获bar变量(注意只捕获bar,其他变量不被捕获)

④[this]捕获当前类中的this指针,让lambda表达式拥有和当前类成员函数同样访问权限,可以使用当前类的成员函数和成员变量(注意也可修改non-const成员变量的值)。


(parameters): Lambda表达式的参数列表(不需要参数传递时,()可以省略)
①如果省略,则类似于无参函数func()。

②参数列表中不能有默认参数。所有的参数必须有参数名。

③不支持可变参数。


opt选项(可省略)
①mutable修饰符:默认下,lambda表达式的operator()是const函数,mutable可以取消其常量性,让body内的代码可以修改被捕获的变量,并可以访问被捕获对象的non-const函数。在使用该修饰符时,参数列表不可省略(即使参数为空)。

②exception:说明lambda表达式是否抛出异常(noexcept),以及抛出何种异常。如抛出整数类型的异常,可以使用throw(int)。

③attribute用来声明属性


-return-type :返回值类型(没有返回值时可省略)
①如果被省略了,则由return语句的返回类型确定

②如果没有return语句,则类似于void func(…)函数。


{statement}:函数体。
在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。


3 初次体验Lambda表达式

在Lambda函数定义中,参数列表和返回值类型都是可选部分,而捕获列表和函数体部分可以为空,所有最简单的Lambda函数为:[]{};但是该Lambda不能做任何事。


省略参数列表和返回值类型

//省略参数列表和返回值类型,返回值类型由编译器自动推导为int
int a = 1, b = 2;
[=] {return a + 3; };//[=] 表示按值捕获 函数体里的a是一份拷贝

省略返回值类型

//省略返回值类型,无返回值类型
int a = 1, b = 2;
auto fun = [&](int c) {b = a + c; };//[&]表示按引用捕获a和b
fun(10);//调用Lambda函数
cout << "a=" << a << endl;//a=1
cout << "b=" << b << endl;//b=11

各部分都很完善的Lambda函数

//各部分都很完善的Lambda函数
int a = 1, b = 2;
//[=,&b]表示除了b按引用捕获,其他都按值捕获
auto fun = [=, &b](int c) {b += a + c; };
fun(10);
cout << "b=" << b << endl;//b=13

mutable

int x = 10;
//mutable表示取消按值捕获的x的常量性,不加mutable无法修改x的值
//但是一般都是捕获x的引用,即[&x]
auto add_x = [x](int a) mutable {return x += a; };
cout << "x=" << add_x(10) << endl;//x=20

通过上述例子可以看出,lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量。


在类中使用Lambda表达式

#include <iostream>using namespace std;int g = 0;class Test
{
private:int i = 0;
public:void func(int x, int y){//auto x1 = []{return i;};   //error,没有捕获任何变量。当然也无法捕获父作用域(func)以外的变量,因此i是不能访问到的!//auto x1 = [&i]{return i;}; //error,无法捕获父作用域(func)以外的变量auto x1 = [=]{return i++;}; //ok,因为“=”或“&”默认会捕获this指针,也就可以访问到成员变量。根据按值捕获的特点,此时//在lambda的函数体内不能修改this指针本身。但这不影响我们通过this指针修改成员变量i的值!auto x2 = []{return g++;};  //ok,g不在lambda的父作用域,不能被捕获。//如auto x2 = [&g]{return g++;}。但由于g是全局变量//所以在lambda的body内仍是可见的!auto x3 = [=]{return i++, i + x + y;};//ok,按值捕获所有外部变量,由于&或=捕获时会默认地同时传入this,所以可改变i的值auto x4 = [&]{return i + x + y;}; //ok,按引用捕获所有变量:x, y以及this指针//但注意,i没有被捕获,也不可捕获到。它是通过this指针来访问到的。auto x5 = [this]{return i++;};    //ok,捕获了this指针,也就可以修改成员的值//auto x6 = [this, x]{return i + x + y;}; //error,没捕获y}
};

4 Lambda函数底层实现原理

仿函数是编译器实现lambda表达式的一种方式。在现阶段,通常编译器会把lambda表达式转化成一个仿函数对象。因此在C++11中,lambda可以视为仿函数的一种等价形式。

在这里插入图片描述

我们以这段代码,转到汇编层面查看它的原理

class Rate
{
public:Rate(double rate) : _rate(rate){}double operator()(double money, int year){return money * _rate * year;}
private:double _rate;
};
int main()
{// 函数对象double rate = 0.49;Rate r1(rate);r1(10000, 2);// Lambdaauto r2 = [=](double monty, int year)->double {return monty * rate * year; };r2(10000, 2);return 0;
}

从使用方式上来看,函数对象与lambda表达式完全一样。
函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可 以直接将该变量捕获到。

在这里插入图片描述

实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。


从底层延申出来的一些问题

1.由于每定义一个lambda表达式,编译器会自动生成一个类,所以Lambda表达式之间是不能互相赋值的,即使看起来类型一样。

int main()
{auto f1 = [] {cout << "hello world" << endl; };auto f2 = [] {cout << "hello world" << endl; };//f1 = f2;   // 编译失败--->提示找不到operator=()// 允许使用一个lambda表达式拷贝构造一个新的副本auto f3(f2);f3();return 0;
}

2.一个Lambda表达式大小由捕获的变量决定,如果无捕获变量,则大小为1字节。

int main()
{int a = 1;auto f1 = [&a] {};auto f2 = [] {};cout << "f1的大小:" << sizeof(f1) << endl;//4cout << "f2的大小:" << sizeof(f2) << endl;//1return 0;
}

原因如该图所示:

在这里插入图片描述
底层会由捕获列表创建出相应的变量。
至于为什么无捕获时大小为1,很简单,C++标准规定的。

想要深究的话可以看看这篇文章
为什么lambda无捕获时大小为1字节

lambda表达式在C++11中被称为“闭包类型(Closure Type)”,可以认为它是一个带有operator()的类(即仿函数),它的捕获列表捕获的任何外部变量最终均会变为闭合类型的成员函数。没有捕获变量的lambda表达式可以直接转换为函数指针,而捕获变量的lambda表达式则不能转换为函数指针。


http://www.ngui.cc/article/show-1200660.html

相关文章

Window环境rabbitmq安装教程

首先我们需要取官网下载对应的两个安装包 第一个是rabbitmq安装包路径在括号里&#xff08;Installing on Windows Manually — RabbitMQ&#xff09; 我们选择window下载即可。 下图是下载的样子&#xff0c;确认是这两个就没问题了 第二个是erlang&#xff08;http://erlang…

C++之C语言基础知识

目录 一 C语言介绍 二 算法 三 数据类型 运算符与表达式 常用数据输入/输出函数 选择结构程序设计 条件运算符 Switch语句&#xff1a; while语句&#xff1a; Do..while语句 For循环语句 转移语句 一 C语言介绍 程序语言的发展历程&#xff1a; 机器语言、汇编语言…

AI狂飙突进,存力需作先锋

5月30日&#xff0c;在2023中关村论坛成果发布会上&#xff0c;《北京市加快建设具有全球影响力的人工智能创新策源地实施方案&#xff08;2023-2025年&#xff09;》正式发布。《实施方案》要求&#xff0c;支持创新主体重点突破分布式高效深度学习框架、大模型新型基础架构等…

day46-动态规划8-单词拆分问题

139.单词拆分-完全背包问题区分求组合数和排列数 本题可以使用回溯算法进行暴力搜索&#xff0c;但是如何使用动态规划的思路进行求解呢。将字符串可以理解成一个容器&#xff0c;将单词可以当成物品&#xff0c;那么此时问题转化成利用物品能否装满容器的问题。这个时候由于返…

淘宝监控竞品sku数据接口

电商竞品数据监控查询可以通过以下几个步骤实现&#xff1a; 确定需要监控的竞品&#xff1a;首先需要明确自己店铺的产品定位和竞争对手&#xff0c;选择需要监控的竞品。 选择监控工具&#xff1a;根据需求和预算选择适合自己的电商竞品数据监控工具&#xff0c;例如谷歌分析…

archive log list :报错的解决办法

装好oracle数据库之后&#xff0c; 没事在练习sql语句&#xff0c; 看看一些基本的字典表啊啥的 但是当我执行 archive log list这个的时候居然给我报错&#xff0c; 这句话的意思是&#xff1a; 查看数据库的备份和恢复策略&#xff0c;并确定归档文件的具体位置&#xff…

Arthas-Class/Classloader相关命令使用

tip&#xff1a;作为程序员一定学习编程之道&#xff0c;一定要对代码的编写有追求&#xff0c;不能实现就完事了。我们应该让自己写的代码更加优雅&#xff0c;即使这会费时费力。 开头&#xff1a; 我们先说下生产使用频率较高的有哪些&#xff1a;dump、jad、mc、retransfo…

Django框架:优缺点、实用场景及与Flask、FastAPI的对比

Django是一个使用Python语言编写的高级Web框架&#xff0c;它提供了快速开发、可重用和可维护的Web应用程序所需的一切组件。在本文中&#xff0c;我们将探讨Django的get和post请求、优缺点、实用场景以及与Flask、FastAPI的对比。 Django的get和post请求 在Django中&#xff0…

leetcode95--不同的二叉搜索树 II(java)

不同的二叉搜索树 II leetcode95 -- 不同的二叉搜索树 II题目描述 解题思路代码演示二叉树专题 leetcode95 – 不同的二叉搜索树 II 原题链接: https://leetcode.cn/problems/unique-binary-search-trees-ii/ 题目描述 给你一个整数 n &#xff0c;请你生成并返回所有由 n 个节…

1036 Boys vs Girls(38行代码)

分数 25 全屏浏览题目 切换布局 作者 CHEN, Yue 单位 浙江大学 This time you are asked to tell the difference between the lowest grade of all the male students and the highest grade of all the female students. Input Specification: Each input file contai…