当前位置: 首页 > news >正文

【C++进阶】C++11 的新特性 | lambda | 包装器

在这里插入图片描述

🫧 励志不掉头发的内向程序员:个人主页

 ✨️ 个人专栏: 《C++语言》《Linux学习》

🌅偶尔悲伤,偶尔被幸福所完善


👓️博主简介:

在这里插入图片描述

文章目录

  • 前言
  • 一、lambda
    • 1.1、lambda 表达式语法
    • 1.2、捕捉列表
    • 1.3、lambda 的应用
    • 1.4、lambda 的原理
  • 二、包装器
    • 2.1、function
    • 2.2、bind
  • 总结


前言

我们目前已经学习了蛮多 C++11 的新特性了,我们再来了解了解我们的 lambda 和 包装器,它们也是十分有用的工具,我们赶紧一起来看看吧。

在这里插入图片描述


一、lambda

1.1、lambda 表达式语法

  • lambda 表达式本质是一个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。lambda 表达式语法使用层而言没有类型,所以我们一般是用 auto 或者模板参数定义的对象去接收 lambda 对象
  • lambda 表达式的格式: [capture-list] (parameters)-> return type { function boby }
  • [capture-list]:捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据 [ ] 来判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下文中的变量供 lambda 函数使用,捕捉列表可以传值和传引用捕捉,具体细节 1.2 中我们再细讲。捕捉列表为空也不能省略
  • (parameters) :参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连同 ( ) 一起省略
  • ->return type:返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。⼀般返回值类型明确情况下,也可省略,由编译器对返回类型进行推导
  • { function boby }:函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略
// ⼀个简单的lambda表达式
auto add1 = [](int x, int y)->int {return x + y; };
cout << add1(1, 2) << endl;

我们可以看到,当我们调用这个 lambda 表达式时,非常像调用一个函数。
在这里插入图片描述

// 1、捕捉为空也不能省略
// 2、参数为空可以省略
// 3、返回值可以省略,可以通过返回对象⾃动推导
// 4、函数题不能省略
auto func1 = []{cout << "hello world" << endl;return 0;};
func1();

我们的捕捉列表和函数体不能省略,返回值和参数如果没有就可以被省略。比如我们写一个可以打印 hello world 的 lambda,就没有参数和返回值,我们就可以把它们都省略掉。
在这里插入图片描述
我们还可以用 lambda 实现一个交换。

int a = 0, b = 1;
auto swap1 = [](int& x, int& y){int tmp = x;x = y;y = tmp;};
swap1(a, b);
cout << a << ":" << b << endl;

写法和我们的交换函数是差不多的。
在这里插入图片描述

1.2、捕捉列表

  • lambda 表达式中默认只能用 lambda 函数体和参数中的变量,如果想用外层作用域中的变量就需要进行捕捉
// 只能用当前lambda局部域和捕捉的对象和全局对象
int a = 0, b = 1, c = 2, d = 3;
auto func1 = [a, &b]{b++;int ret = a + b;return ret;};

此时我们的 a 和 b 就是外部变量,我们必须要捕捉才能使用,如果不捕捉就会报错。
在这里插入图片描述

  • 第一种捕捉方式是在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉的多个变量用逗号分割。[x,y, &z] 表示 x 和 y 值捕捉,z 引用捕捉。

我们上面的 a 就是传值捕捉, 而 b 就是传引用捕捉。同理,我们的值捕捉的变量不能修改,而引用捕捉的变量可以修改。

int a = 0, b = 1, c = 2, d = 3;
auto func1 = [a, &b]{// 值捕捉的变量不能修改,引⽤捕捉的变量可以修改//a++;b++;int ret = a + b;return ret;};
cout << func1() << endl;
cout << a << ":" << b << endl;

在这里插入图片描述

  • 第二种捕捉方式是在捕捉列表中隐式捕捉,我们在捕捉列表写一个 = 表示隐式值捕捉,在捕捉列表写一个 & 表示隐式引用捕捉,这样我们 lambda 表达式中用了那些变量,编译器就会自动捕捉那些变量。
// 隐式值捕捉
// 用了哪些变量就捕捉哪些变量
auto func2 = [=]{int ret = a + b + c;return ret;};
cout << func2() << endl;// 隐式引用捕捉
// 用了哪些变量就捕捉哪些变量
auto func3 = [&]{a++;c++;d++;};
func3();
cout << a << " " << b << " " << c << " " << d << endl;

在这里插入图片描述

  • 第三种捕捉方式是在捕捉列表中混合使用隐式捕捉和显示捕捉。[ =, &x ] 表示其他变量隐式值捕捉,x 引用捕捉;[ &, x, y ] 表示其他变量引用捕捉,x 和 y 值捕捉。当使用混合捕捉时,第一个元素必须是 & 或 =,并且 & 混合捕捉时,后面的捕捉变量必须是值捕捉,同理 = 混合捕捉时,后面的捕捉变量必须是引用捕捉。
auto func4 = [&, a, b]{//a++;  // 由于是值捕捉,所以不//b++;  // 能加减,否则报错c++;d++;return a + b + c + d;};
func4();
cout << a << " " << b << " " << c << " " << d << endl;
// 混合捕捉2
auto func5 = [=, &a, &b]{a++;b++;//c++;  // 由于是值捕捉,所以不//d++;  // 能加减,否则报错return a + b + c + d;};
func5();
cout << a << " " << b << " " << c << " " << d << endl;

在这里插入图片描述

  • lambda 表达式如果在函数局部域中,他可以捕捉 lambda 位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉, lambda 表达式中可以直接使用。这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。
int x = 0;
int main()
{// 局部的静态和全局变量不能捕捉,也不需要捕捉static int m = 1;auto func6 = []{int ret = x + m;return ret;};cout << func6() << endl;
}

在这里插入图片描述

  • 默认情况下,lambda 捕捉列表是被 const 修饰的,也就是说传值捕捉的过来的对象不能修改,mutable 加在参数列表的后面可以取消其常量性,也就说使用该修饰符后,传值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参。使用该修饰符后,参数列表不可省略(即使参数为空)。
// 传值捕捉本质是⼀种拷贝,并且被 const 修饰了
// mutable 相当于去掉 const 属性,可以修改了
// 但是修改了不会影响外⾯被捕捉的值,因为是⼀种拷贝
auto func7 = [=]()mutable{a++;b++;c++;d++;return a + b + c + d;};
cout << func7() << endl;
cout << a << " " << b << " " << c << " " << d << endl;

在这里插入图片描述
这里外面的 a、b、c、d、没有改变,只有 lambda 内部发生改变。

1.3、lambda 的应用

在学习 lambda 表达式之前,我们的使用的可调用对象只有函数指针和仿函数对象,函数指针的类型定义起来比较麻烦,仿函数要定义一个类,相对会比较麻烦。使用 lambda 去定义可调用对象,既简单又方便。

lambda 在很多其他地方用起来也很好用。比如线程中定义线程的执行函数逻辑,智能指针中定制删除器等, lambda 的应用还是很广泛的,以后我们会不断接触到。

struct Goods
{string _name; // 名字double _price; // 价格int _evaluate; // 评价// ...Goods(const char* str, double price, int evaluate):_name(str), _price(price), _evaluate(evaluate){}
};struct ComparePriceLess
{bool operator()(const Goods& gl, const Goods& gr){return gl._price < gr._price;}
};struct ComparePriceGreater
{bool operator()(const Goods& gl, const Goods& gr){return gl._price > gr._price;}
};int main()
{vector<Goods> v = { { "苹果", 2.1, 5 }, { "⾹蕉", 3, 4 }, { "橙⼦", 2.2, 3}, { "菠萝", 1.5, 4 } };// 类似这样的场景,我们实现仿函数对象或者函数指针⽀持商品中// 不同项的比较,相对还是比较麻烦的,那么这⾥ lambda 就很好用了sort(v.begin(), v.end(), ComparePriceLess());sort(v.begin(), v.end(), ComparePriceGreater());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;});sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._evaluate < g2._evaluate;});sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._evaluate > g2._evaluate;});return 0;
}

此时如果我们去实现仿函数,或者说旁边有注释说明这个仿函数的含义还好,如果我们在使用排序的逻辑时,既不是自己写的逻辑,也没有写注释,那就很麻烦了,我们就得去找这个仿函数的含义到底是什么。此时我们直接使用 lambda 表达式,让人一眼就能知道逻辑是什么,此时这串代码就可以省去别人去各个文件中寻找的时间了。

1.4、lambda 的原理

  • lambda 的原理和范围 for 很像,编译后从汇编指令层的角度看,压根就没有 lambda 和范围 for 这样的东西。范围 for 底层是迭代器,而lambda 底层是仿函数对象,也就说我们写了一个 lambda 以后,编译器会生成一个对应的仿函数的类。

  • 仿函数的类名是编译按一定规则生成的,保证不同的 lambda 生成的类名不同,lambda 参数/返回类型/函数体就是仿函数 operator() 的参数/返回类型/函数体, lambda 的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参,当然隐式捕捉,编译器要看使用哪些就传那些对象。

// 仿函数
class Rate
{
public:Rate(double rate): _rate(rate){}double operator()(double money, int year){return money * _rate * year;}
private:double _rate;
};
// lambda
double rate = 0.49;
auto r2 = [rate](double money, int year) {return money * rate * year;};

我们尝试调用上面的仿函数和 lambda。

Rate r1(rate);
cout << r1(10000, 2) << endl;
cout << r2(10000, 2) << endl;

在这里插入图片描述
本质上它们是一样的。


二、包装器

2.1、function

在这里插入图片描述

  • std::function 是一个类模板,也是一个包装器。std::function 的实例对象可以包装存储其他的可以调用对象,包括函数指针、仿函数、 lambda、bind 表达式等,存储的可调用对象被称为 std::function 的目标。若 std::function 不含目标,则称它为空。调用空 std::function 的目标导致抛出 std::bad_function_call 异常。function 被定义 < functional > 头⽂件中。
int f(int a, int b)
{return a + b;
}
struct Functor
{
public:int operator() (int a, int b){return a + b;}
};

在我们的 function 中,( ) 里保存的是参数类型,而 <> 里 ( ) 外保存的就是返回值类型。

// 包装各种可调⽤对象
function<int(int, int)> f1 = f;
function<int(int, int)> f2 = Functor();
function<int(int, int)> f3 = [](int a, int b) {return a + b; };
cout << f1(1, 1) << endl;
cout << f2(1, 1) << endl;
cout << f3(1, 1) << endl;

在这里插入图片描述

我们第一个包装器想要保存 f。它是一个返回值为 int,参数为 int,int。所以包装器的写法就是 <int (int, int)>。而第二个也是如此,它的调用方式也和第一个是一样的。只要我们的可调用类型和我们包装器的参数是匹配的,就可以被包装,否则就会报错。
在这里插入图片描述
除了全局函数,我们还可以包装一个静态成员函数,但是与全局不同的地方在于,静态成员函数的包装需要指定类域。

class Plus
{
public:Plus(int n = 10):_n(n){}static int plusi(int a, int b){return a + b;}double plusd(double a, double b){return (a + b) * _n;}
private:int _n;
};
// 包装静态成员函数
// 成员函数要指定类域并且前⾯加&才能获取地址
function<int(int, int)> f4 = &Plus::plusi;
cout << f4(1, 1) << endl;

我们这里除了指定类域,前面还加了一个 &,这是因为规定非静态的成员函数想要包装必须前面加一个 &,但是静态成员函数可加可不加,但是建议还是加。

// 包装普通成员函数
// 普通成员函数还有⼀个隐含的this指针参数,所以绑定时传对象或者对象的指针过去都可以
function<double(Plus*, double, double)> f5 = &Plus::plusd;
Plus pd;
cout << f5(&pd, 1.1, 1.1) << endl;

在这里插入图片描述
注意我们的成员函数前面默认有一个 this 指针,为了参数匹配我们也得加一个指针在前面。

除了上面的写法,我们还可以这样包装成员函数。

function<double(Plus, double, double)> f6 = &Plus::plusd;
Plus pd;
cout << f6(pd, 1.1, 1.1) << endl;

成员函数是用一个 .* 运算符调用的,它会显示传递对象的地址,如果是指针就直接传递,如果是对象它会自动取地址。

也可以用右值引用。

function<double(Plus&&, double, double)> f7 = &Plus::plusd;
Plus pd;
cout << f7(move(pd), 1.1, 1.1) << endl;
cout << f7(Plus(), 1.1, 1.1) << endl;

这些写法都是可以的,大家看着使用即可。

  • 函数指针、仿函数、 lambda 等可调用对象的类型各不相同, std::function 的优势就是统一类型,对他们都可以进行包装,这样在很多地方就方便声明可调用对象的类型。

2.2、bind

在这里插入图片描述

  • bind 是一个函数模板,它也是⼀个可调用对象的包装器,可以把他看做一个函数适配器,对接收的 fn 可调用对象进行处理后返回一个可调用对象。 bind 可以用来调整参数个数和参数顺序。bind 也在 < functional > 这个头文件中。

  • 调用 bind 的一般形式: auto newCallable = bind(callable,arg_list); 其中 newCallable 本身是一个可调用对象,arg_list 是一个逗号分隔的参数列表,对应给定的 callable 的参数。当我们调用 newCallable 时,newCallable 会调用 callable,并传给它 arg_list 中的参数。

  • arg_list 中的参数可能包含形如 _n 的名字,其中 n 是一个整数,这些参数是占位符,表示 newCallable 的参数,它们占据了传递给newCallable 的参数的位置。数值 n 表示生成的可调用对象中参数的位置:_1 为 newCallable 的第一个参数,_2 为第二个参数,以此类推。_1/_2/_3… 这些占位符放到 placeholders 的⼀个命名空间中。
    在这里插入图片描述

using placeholders::_1;
using placeholders::_2;
using placeholders::_3;
int Sub(int a, int b)
{return (a - b) * 10;
}
int SubX(int a, int b, int c)
{return (a - b - c) * 10;
}
class Plus
{
public:static int plusi(int a, int b){return a + b;}double plusd(double a, double b){return a + b;}
};

首先,这个用法可以调整参数的顺序。

auto sub1 = bind(Sub, _1, _2);
cout << sub1(10, 5) << endl;
auto sub2 = bind(Sub, _2, _1);
cout << sub2(10, 5) << endl;

第一个参数传的是可调用对象,后面传的就是占位符,_1 代表第一个参数,_2 代表第二个参数。它的返回值是一个仿函数,可以用 auto 或者 function 接收。
在这里插入图片描述
其次它还可以用来调整参数个数。

// 调整参数个数 (常⽤)
auto sub3 = bind(Sub, 100, _1);
cout << sub3(5) << endl;
auto sub4 = bind(Sub, _1, 100);
cout << sub4(5) << endl;
// 分别绑死第123个参数
auto sub5 = bind(SubX, 100, _1, _2);
cout << sub5(5, 1) << endl;
auto sub6 = bind(SubX, _1, 100, _2);
cout << sub6(5, 1) << endl;
auto sub7 = bind(SubX, _1, _2, 100);
cout << sub7(5, 1) << endl;

我们想把一个参数固定死,我们只要在对应位置输入我们要固定的值,那那个位置就会被固定死了。剩下的位置我们就会重新分配占位符,还是从 _1 开始,但是以 N(总参数数量) - M(绑定数数量)为结尾。

在这里插入图片描述
我们可以利用这个 bind 把上面的包装器优化一下。

function<double(Plus&&, double, double)> f6 = &Plus::plusd;
Plus pd;
cout << f6(move(pd), 1.1, 1.1) << endl;
cout << f6(Plus(), 1.1, 1.1) << endl;
// bind⼀般⽤于,绑死⼀些固定参数
function<double(double, double)> f7 = bind(&Plus::plusd, Plus(), _1, _2);
cout << f7(1.1, 1.1) << endl;

我们把我们的 this 指针的位置绑定死,这样就没有必要每次都传了。



总结

以上便是我们 C++11 的一些比较有用的内容和知识点了,我们一定要对其认真学习了解,下一章节我们来了解了解我们的异常是什么,我们下一章节再见。

在这里插入图片描述

🎇坚持到这里已经很厉害啦,辛苦啦🎇
ʕ • ᴥ • ʔ
づ♡ど
http://www.dtcms.com/a/392351.html

相关文章:

  • 2.【QT 5.12.12 安装 Windows 版本】
  • Rust_2025:阶段1:day6.3 macro
  • 【Qt开发】输入类控件(一)-> QLineEdit
  • python10——组合数据类型(集合)
  • 分布式专题——14 RabbitMQ之集群实战
  • WEEX唯客的多维度安全守护
  • 深度学习环境配置
  • 生鲜速递:HTTP 的缓存控制
  • ​​Snipaste 2.10.1.dmg截图安装教程|Mac电脑拖拽安装详细步骤​
  • 10.1.1 使用python完成第一个遗传算法
  • C语言内存精讲系列(二十九):C 语言堆区内存进阶与动态内存实战
  • 6G量子通信融合:破解未来网络的安全与效能密码
  • C#练习题——泛型实现单例模式和增删改查
  • 网关登录校验
  • Kubernetes Fluent Bit Pod Pending 问题解决方案
  • 我爱学算法之—— 位运算(中)
  • 什么是差分信号
  • 相机标定(Camera Calibration)原理及步骤:从 “像素模糊” 到 “毫米精准” 的关键一步
  • 用 【C# + WinUI3 + 图像动画】 来理解:高数 - 函数 - 初等函数
  • ​​[硬件电路-296]:单刀双掷(SPDT)模拟开关
  • 【MAVLink】MAVSDK编程入门、基本概念
  • MAC-基于反射的枚举工具类优化
  • 防御性编程:编程界的‘安全驾驶‘指南
  • Qt绘图方式有哪些
  • 使用python创建、管理、分析和可视化“欲望”之间的关系网络图
  • 铸铁平台:工业制造的基石与精密测量的核心
  • Mac环境安装Nginx指南实录
  • 《RAG是什么?为什么它比微调更适合让AI拥有“专业知识”?》
  • 【Python】控制台界面演示
  • 软考中级习题与解答——第九章_信息安全(2)