【C++】深入剖析C++中的lambda表达式&&包装器&&bind

目录

一、lambda表达式

1、引入

2、lambda表达式

3、lambda表达式语法

[​4、lambda 的底层逻辑](#4、lambda 的底层逻辑)

二、包装器

1、包装器的表达式

​ 2、实例化多份

3、可调用对象类型

4、实操例题

三、bind

[1、bind 的表达式](#1、bind 的表达式)

2、调整参数的位置

3、绑定参数


一、lambda表达式

1、引入

在C++98中,如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法。

如果待排序元素为自定义类型,需要用户定义排序时的比较规则:

首先直接将自定义类型放入sort里进行排序肯定是不行的,sort里的排序默认是根据int类型的大小进行比较的。所以这时如果想要对商品进行排序就需要使用仿函数。利用仿函数来进行比较。如果要更新比较规则就需要重新书写仿函数。

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

2、lambda表达式

cpp 复制代码
int main()
 {
 vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };
 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; });
 }

原本需要重写一个类,现在只要写一行类似表达式的东西就可以了。接下来我们就解析这行"表达式"。从上下面对比,这行"表达式"本质上就是一个匿名函数对象。

3、lambda表达式语法

lambda表达式书写格式:capture-list (parameters) mutable -> return-type { statement }

  • capture-list : 捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据 来判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下文中的变量供 lambda 函数使用。
  • (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同 ()一起省略
  • mutable:默认情况下,lambda 函数总是一个 const 函数,mutable 可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
  • ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
  • {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。

注意:在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为 空。因此C++11中最简单的lambda函数为:\[\]{}; 该lambda函数不能做任何事情。

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

捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。

  • var:表示值传递方式捕捉变量var
  • =:表示值传递方式捕获所有父作用域中的变量(包括this)
  • \&var:表示引用传递捕捉变量var
  • \&:表示引用传递捕捉所有父作用域中的变量(包括this)
  • this:表示值传递方式捕捉当前的this指针

4、lambda 的底层逻辑

其实lambda表达式并不神奇,它底层就是仿函数 。实际在底层编译器对应 lambda 表达式的处理上,可以像函数一样使用的对象。函数对象是如何处理的呢?就是定义一个类,类里重载了()运算符重载。然后利用这个类定义个对象,该对象可以像函数一样被调用。所以如果定义了一个lambda表达式,编译器就会自动生成一个类,并在这个类里重载()运算符。

注意:

a. 父作用域指包含 lambda 函数的语句块

b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。 比如:

=, \&a, \&b:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量

\&,a, this:值传递方式捕捉变量a和this,引用方式捕捉其他变量

c. 捕捉列表不允许变量重复传递,否则就会导致编译错误。 比如:=, a:=已经以值传递方式捕捉了所有变量,捕捉a重复

d. 在块作用域以外的 lambda 函数捕捉列表必须为空。

e. 在块作用域中的 lambda 函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。

f. 不同lambda函数的底层的创建的类名称是不一样的。所以 lambda 函数之间是不可以互相赋值的(不同类型,无法赋值)。

二、包装器

function包装器也叫作适配器。C++中的 function 本质是一个类模板,也是一个包装器。

可调用对象有三个:1.函数指针 2.函数对象 3.lambda函数

1、包装器的表达式

cpp 复制代码
std::function在头文件<functional>
 // 类模板原型如下
template <class T> function;     
// undefined
 template <class Ret, class... Args>
 class function<Ret(Args...)>;
模板参数说明:
Ret: 被调用函数的返回类型
Args...:被调用函数的形参

2、实例化多份

可调用对象存在多种,当我们写一个需要传可调用对象参数的类时,使用模板,当传不同的可调用对象时就会实例化出不同的类模板,造成模板使用效率低效。

我们会发现useF函数模板实例化了三份。

当我们使用包装器时,将可调用对象:函数指针,函数对象,lambda函数,都可以封装在包装器里。然后我们就可以统一调用不同的包装器(不同的包装器里包装着不同的可调用对象)。虽然是不同的包装器但是同一类型,所有最后只会实例化出一份。

3、可调用对象类型

想要将可调用对象存储到容器里,首先我们得需要知道它的类型,函数指针的类型实在是太麻烦了,而仿函数类型我们是可以知道,但lambda的类型我们是不知道的,所以难道容器里只能存储仿函数吗?不能存储lambda函数。

包装器就可以解决可调用对象的类型问题,它可以将函数指针,函数对象,lambda包装起来,并且这个包装的类型我们是知道的。那么我们就可以利用这个包装器将 lambda 包装起来,然后再存储这个包装器即可,这样 lambda 函数就被存储到容器里了。不仅是 lambda 函数被存储到容器里了,是所有的可调用对象都可以被存储到容器里了。

4、实操例题

【150】逆波兰表达式求值

给你一个字符串数组 tokens ,表示一个根据 逆波兰表示法 表示的算术表达式。

请你计算该表达式。返回一个表示表达式值的整数

【普通版本】

cpp 复制代码
class Solution {
public:
    int evalRPN(vector<string>& tokens) {
    stack<int> st;
 for(auto& str : tokens)
{
  if(str == "+" || str == "-" || str == "*" || str == "/")
  {
    int right = st.top();
    st.pop();
    int left = st.top();
    st.pop();
    switch(str[0])
    {
     case '+':
     st.push(left+right);
     break;
     case '-':
     st.push(left-right);
     break;
     case '*':
     st.push(left*right);
     break;
     case '/':
     st.push(left/right);
     break;
    }
  }
  else
  {
   st.push(stoi(str));
  }
}  
return st.top();
 }
};

【包装器版本】

cpp 复制代码
class Solution {
public:
    int evalRPN(vector<string>& tokens) {
    stack<int> st;
 map<string,function<int(int,int)>> cmdOP=
 {
    {"+",[](int x,int y){return x+y;}},
    {"-",[](int x,int y){return x-y;}},
    {"*",[](int x,int y){return x*y;}},
    {"/",[](int x,int y){return x/y;}},
 };

 for(auto& str:tokens)
 {
    if(cmdOP.count(str))
    {
        int right=st.top();
        st.pop();
        int left=st.top();
        st.pop();
        st.push(cmdOP[str](left,right));
    }
    else
    {
        st.push(stoi(str));
    }
 }
return st.top();
 }
};

三、bind

std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来"适应"原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M 可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作。

1、bind 的表达式

cpp 复制代码
// 原型如下:
template <class Fn, class... Args>
 /* unspecified */ bind (Fn&& fn, Args&&... args);
 // with return type (2) 
template <class Ret, class Fn, class... Args>
 /* unspecified */ bind (Fn&& fn, Args&&... args);

可以将 bind 函数看作是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对 象来"适应"原对象的参数列表。

调用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为第二个参数,以此类推

2、调整参数的位置

通过调整bind的占位符顺序,就可以调整函数的参数位置了。因为第一个实参设定传给的就是占位符1,第二个实参设定传给的就是占位符2.而占位符则是按照顺序传给函数的形参。

3、绑定参数

bind不仅可以调整可调用对象的参数位置。(通过包装可调用对象适配出想要的参数位置)。

还可以用来固定参数值。类似于缺省参数的功能。在包装这个可调用对象时就可以将对象的参数固定。而不需要去对象的内部。

但它不像缺省参数,缺省参数是写死了,只能定义一种类型的函数。(因为缺省参数需要在函数内部写,一旦函数内部写完,外部就无法改动了)

但 bind 可以灵活的调整可调用对象参数的值,不需要到函数里面去改动,直接在函数外面调整就可以同时写出多个不同需求的函数。

相关推荐
地平线开发者8 小时前
profiler debug 工具用法与高一致性策略
算法·自动驾驶
编程大师哥8 小时前
匿名函数 lambda + 高阶函数
java·python·算法
isyangli_blog8 小时前
OpenDayLight (Carbon 版本) 启动与组件安装
开发语言·php
vb2008118 小时前
FastAPI APIRouter
开发语言·python
Benszen8 小时前
KVM虚拟化解决方案
开发语言·perl
会编程的土豆8 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
東雪木8 小时前
多线程与并发编程 专属复习笔记
java·开发语言·笔记·java面试
我叫袁小陌8 小时前
算法解题思路指南
算法
MC皮蛋侠客8 小时前
C++17 多线程系列(五):C++17 并行算法——从串行到并行的零成本迁移
c++·多线程
地平线开发者8 小时前
Conv+BN+Add+ReLU 融合机制简介
算法·自动驾驶