C++11的更新介绍(lamada、包装器)

2024-04-13 15:28
文章标签 c++ 介绍 更新 包装 lamada

本文主要是介绍C++11的更新介绍(lamada、包装器),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

 

🪐🪐🪐欢迎来到程序员餐厅💫💫💫

          主厨:邪王真眼

主厨的主页:Chef‘s blog  

所属专栏:c++大冒险

总有光环在陨落,总有新星在闪烁


lambda表达式

C++98中的一个例子

在C++98中,如果想要进行排序,可以使用std::sort方法如果待排序元素为自定义类型,需要用户定义排序时的比较规则:

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 } };sort(v.begin(), v.end(), ComparePriceLess());
sort(v.begin(), v.end(), ComparePriceGreater());
}

随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法,

都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,

这些都给编程者带来了极大的不便。因此,在C++11语法中出现了Lambda表达式。


lambda表达式语法

  • lambda表达式书写格式:

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

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

1.mutable可省略

int a = 0, b = 0;
auto func = [a, b]()->int {return 0; };

2.返回值类型可省略,编译器自动推导

int a = 0, b = 0;
auto func = [a, b]() {return 0; };

3.没有传参时列表可省略

int a = 0, b = 0;
auto func = [a, b] {return 0; };

4.而捕捉列表和函数体可以为空。

 // 省略了返回值类型,无返回值类型auto fun1 = [](int c){ }; 

因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。

lambda表达式返回值实际上是仿函数,

该仿函数无法直接调用,如果想要调用,可借助auto将其赋值给一个变量,lambda返回的仿函数对象,其类名是随机的,因此必须用auto来接受这个仿函数对象。


捕获列表说明

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

1.[var]:表示值传递方式捕捉变量var

int a = 0, b = 0;
auto func = [a, b] (){return a+b};

注意:此时a,b具有常性,如果要去掉他们的常性就要加上mutable,此时lambda获得ab的方式是传值,所以不会影响到父作用域的ab

int a = 0, b = 0;
auto func = [a, b] ()mutable{return a+b};

2.[this]:表示值传递方式捕捉当前的this指针

3.[=]:表示值传递方式捕获所有父作用域中的变量(包括this)

int a = 0, b = 0;
auto func = [=]()mutable {return a + b; };

4.[&var]:表示引用传递捕捉变量var

此时lambda获得ab的方式是传引用,在lambda里修改ab会影响父作用域的ab

int a = 0, b = 0;
auto func = [&a,&b]{return a + b; };

5.[&]:表示引用传递捕捉所有父作用域中的变量(包括this)

int a = 0, b = 0;
auto func = [&]() {return a + b; };

6.我们还可以把传值和传引用混合使用,让部分参数传参,部分参数传引用

[x, &y]:以传值的形式捕获x,以传引用的形式捕获y
[=, &x]:以传值的形式捕获父作用域所有变量,以传引用的形式捕获x
[&, x]:以传值的形式捕获x,以传引用的形式捕获父作用域所有变量

注意:

  1.  父作用域指包含lambda函数的语句块
  2. c捕捉列表不允许变量重复传递,否则就会导致编译错误。 比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
  3. 在块作用域以外的lambda函数捕捉列表必须为空
  4. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
  5.  lambda表达式之间不能相互赋值,因为他们的实际类名不同(每一个类名都是lamdba随机生成的)

有lambda后,我们在需要仿函数的地方,就无需额外写一个仿函数的类,而是直接写lambda表达式:

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

函数对象与lambda表达式

函数对象,又称为仿函数,即可以想函数一样使用的对象,就是在类中重载了operator()运算符的

类对象。

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);
// lamberauto r2 = [=](double monty, int year)->double{return monty*rate*year; 
};r2(10000, 2);return 0;
}

从使用方式上来看,函数对象与lambda表达式完全一样。

函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可

以直接将该变量捕获到。 ​

  

实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如

果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator(),而这个类的名字是编译器随机产生的,所以lambda表达式之间不能相互赋值,即使看起来类型相同,因为他们的类名是不同的


模板参数中的lambda表达式

我们要给set传一个仿函数

set<int, Less<int>>;

请注意,这里我们所传的不是对象,而是类,但是lambda返回值本身就是对象,所以直接传lambda肯定是不可以的

这个时候decltype就登场了

auto func = [](int a, int b) {return a < b; };
set<int, decltype(func)>;

包装器

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

为什么需要function

如果一个变量f,可以按f()的形式调用函数,那么称f是一个可调用对象

基于此不难想到可调用对象包括:函数、仿函数、lambda

我们来看看他们在下面代码的表现

template<class F, class T>
T useF(F f, T x)
{static int count = 0;cout << "count:" << ++count << endl;cout << "count:" << &count << endl;return f(x);
}
double f(double i)
{return i / 2;
}
struct Functor
{double operator()(double d){return d / 3;}
};
int main()
{// 函数cout << useF(f, 11.11) << endl;// 函数对象cout << useF(Functor(), 11.11) << endl;// lamber表达式cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;return 0;
}

通过上面的程序验证,我们会发现useF函数模板实例化了三份

然而,这里的函数、仿函数、lambda的返回值和参数类型相同,可以认为三者极其相似,有没有办法让函数模板把他们识别为一种类型,从而只需要实例化一份呢

包装器可以很好的解决上面的问题,function包含在头文件<functional>中,是一个类模板,模板原型如下:

template <class T> function;template <class Ret, class... Args>
class function<Ret(Args...)>;

其语法为:function<返回值(参数列表)>,只要是返回值和参数列表相同的可调用对象,经过这一层封装,都会变成相同的类型。 

int f(int a, int b)
{return a + b;
}
struct Functor
{
public:int operator() (int a, int b){return a + b;}
};
template<class F>
void Function(F f)
{static int count = 0;cout << count++<<endl;cout << &count << endl;cout << "================="<<endl;
}
int main()
{// 函数名(函数指针)function<int(int, int)> func1 = f;Function(func1);// 函数对象function<int(int, int)> func2 = Functor();Function(func1);// lamber表达式function<int(int, int)> func3 = [](const int a, const int b){return a + b; };Function(func1);
}

可以看出func1、func2、func3被认为是一种类型


function接收对象成员函数

class Plus
{
public:static int plusi(int a, int b){return a + b;}double plusd(double a, double b){return a + b;}
};
int main()
{// 类的成员函数function<int(int, int)> func4 = &Plus::plusi;function<double(Plus*, double, double)> func5 = &Plus::plusd;
function<double(Plus, double, double)> func5 = &Plus::plusd;
}

注意事项:

  1. 等号右边的&符号别忘了写,对于非静态函数必需加上,非静态最好加上
  2. 等号右边的函数要指定类域
  3. 对于非静态函数,左边的第一个参数是类名或类指针

    适用场景:

比如力扣:波兰表达式

对于相邻两数我们要以case语句对+-*/进行讨论

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;}}//..........}return st.top();
}
};

使用包装器以后的玩法 

class Solution {
public:
int evalRPN(vector<string>& tokens) {stack<int> st;map<string, function<int(int, int)>> opFuncMap ={{ "+", [](int i, int j){return i + j; } },{ "-", [](int i, int j){return i - j; } },{ "*", [](int i, int j){return i * j; } },{ "/", [](int i, int j){return i / j; } }};for(auto& str : tokens){if(opFuncMap.find(str) != opFuncMap.end()){int right = st.top();st.pop();int left = st.top();st.pop();st.push(opFuncMap[str](left, right));}//........}return st.top();
}
};

冷知识:

  • function 类型相同的对象可以相互赋值

function<int(int)> f1 = [](int x) { return x * x; };
function<int(int)> f2 = f1; // f2 现在也是一个 lambda 表达式
  • function实现了对bool的重载

opearotr bool函数重载方式如下

class A
{
public:operator bool(){return 3==_a;}int _a = 2;
};int main()
{A a;if (a)cout << "666" << endl;else{cout << "888" << endl;}
}

  

function 对象支持 bool 类型转换,可以用于判断 function 对象是否为空(未初始化)。

function<int(int)> f;
if (!f) {cout << "f is empty" << endl;
}

  


bind

bind音译即是绑定,它是C++标准库中的一个函数模板,用于将函数与其参数进行绑定,生成一个新的可调用对象。通过bind,我们可以将函数的部分参数固定下来,也可以交换参数位置,从而得到一个新的函数对象。

// 原型如下:
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的一般形式

auto newCallable = bind(callable,arg_list);

newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数

  • 交换参数位置

交换了第一个参数和第二个参数的位置

C++11后新增一个命名空间域placeholders,其内部会存储很多变量,这些变量用于函数的传参

placeholders::_n表示原函数中的第n个参数

#include <functional>
void Mod(int a, int b)
{cout << a % b<<endl;
}
int main()
{std::function<void(int, int)> func1 = std::bind(Mod, placeholders::_2,placeholders::_1);func1(2, 1);Mod(2, 1);return 0;
}

  


  •   给参数绑定固定值

void Mod(int a, int b)
{cout << a % b<<endl;
}
int main()
{std::function<void(int,int)> func1 = std::bind(Mod, 1, placeholders::_2);func1(2, 10);Mod(1, 10);return 0;
}

  

可以看出即使我们在func1中给第一个参数传参为2,实际也依旧是1,

三种写法:

function<void(int,int)> func1 = std::bind(Mod, 1, placeholders::_1);
func1(1,10);
function<void(int)> func2 = std::bind(Mod, 1, placeholders::_1);
func2(10);
auto func3 = std::bind(Mod, 1, placeholders::_1);
func3(1, 10);
func3(10);

  我建议是auto,它的适用范围最大


🥰创作不易,你的支持对我最大的鼓励🥰

🪐~ 点赞收藏+关注 ~🪐

e3ff0dedf2ee4b4c89ba24e961db3cf4.gif

这篇关于C++11的更新介绍(lamada、包装器)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/900505

相关文章

java脚本使用不同版本jdk的说明介绍

《java脚本使用不同版本jdk的说明介绍》本文介绍了在Java中执行JavaScript脚本的几种方式,包括使用ScriptEngine、Nashorn和GraalVM,ScriptEngine适用... 目录Java脚本使用不同版本jdk的说明1.使用ScriptEngine执行javascript2.

Redis缓存问题与缓存更新机制详解

《Redis缓存问题与缓存更新机制详解》本文主要介绍了缓存问题及其解决方案,包括缓存穿透、缓存击穿、缓存雪崩等问题的成因以及相应的预防和解决方法,同时,还详细探讨了缓存更新机制,包括不同情况下的缓存更... 目录一、缓存问题1.1 缓存穿透1.1.1 问题来源1.1.2 解决方案1.2 缓存击穿1.2.1

C++中实现调试日志输出

《C++中实现调试日志输出》在C++编程中,调试日志对于定位问题和优化代码至关重要,本文将介绍几种常用的调试日志输出方法,并教你如何在日志中添加时间戳,希望对大家有所帮助... 目录1. 使用 #ifdef _DEBUG 宏2. 加入时间戳:精确到毫秒3.Windows 和 MFC 中的调试日志方法MFC

Python实现NLP的完整流程介绍

《Python实现NLP的完整流程介绍》这篇文章主要为大家详细介绍了Python实现NLP的完整流程,文中的示例代码讲解详细,具有一定的借鉴价值,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1. 编程安装和导入必要的库2. 文本数据准备3. 文本预处理3.1 小写化3.2 分词(Tokenizatio

Linux Mint Xia 22.1重磅发布: 重要更新一览

《LinuxMintXia22.1重磅发布:重要更新一览》Beta版LinuxMint“Xia”22.1发布,新版本基于Ubuntu24.04,内核版本为Linux6.8,这... linux Mint 22.1「Xia」正式发布啦!这次更新带来了诸多优化和改进,进一步巩固了 Mint 在 Linux 桌面

SpringCloud配置动态更新原理解析

《SpringCloud配置动态更新原理解析》在微服务架构的浩瀚星海中,服务配置的动态更新如同魔法一般,能够让应用在不重启的情况下,实时响应配置的变更,SpringCloud作为微服务架构中的佼佼者,... 目录一、SpringBoot、Cloud配置的读取二、SpringCloud配置动态刷新三、更新@R

深入理解C++ 空类大小

《深入理解C++空类大小》本文主要介绍了C++空类大小,规定空类大小为1字节,主要是为了保证对象的唯一性和可区分性,满足数组元素地址连续的要求,下面就来了解一下... 目录1. 保证对象的唯一性和可区分性2. 满足数组元素地址连续的要求3. 与C++的对象模型和内存管理机制相适配查看类对象内存在C++中,规

Ubuntu 24.04 LTS怎么关闭 Ubuntu Pro 更新提示弹窗?

《Ubuntu24.04LTS怎么关闭UbuntuPro更新提示弹窗?》Ubuntu每次开机都会弹窗提示安全更新,设置里最多只能取消自动下载,自动更新,但无法做到直接让自动更新的弹窗不出现,... 如果你正在使用 Ubuntu 24.04 LTS,可能会注意到——在使用「软件更新器」或运行 APT 命令时,

在 VSCode 中配置 C++ 开发环境的详细教程

《在VSCode中配置C++开发环境的详细教程》本文详细介绍了如何在VisualStudioCode(VSCode)中配置C++开发环境,包括安装必要的工具、配置编译器、设置调试环境等步骤,通... 目录如何在 VSCode 中配置 C++ 开发环境:详细教程1. 什么是 VSCode?2. 安装 VSCo

C++11的函数包装器std::function使用示例

《C++11的函数包装器std::function使用示例》C++11引入的std::function是最常用的函数包装器,它可以存储任何可调用对象并提供统一的调用接口,以下是关于函数包装器的详细讲解... 目录一、std::function 的基本用法1. 基本语法二、如何使用 std::function