[C++11] 包装器 : function 与 bind 的原理及使用


文章目录

  • `function`
    • `std::function` 的基本语法
    • 使用 `std::function` 包装不同的可调用对象
    • `function`包装普通成员函数为什么要传入 `this` 指针参数?
      • 传入对象指针与传入对象实例的区别
    • 例题 :150. 逆波兰表达式求值 - ⼒扣(LeetCode)
  • `bind`
    • `std::bind` 的基本语法
    • `std::bind` 参数的顺序调整与绑定
      • 顺序调整
      • 参数的绑定
  • `std::function` 和 `std::bind` 的实际应用
      • 结论

function

std::function 是⼀个类模板,也是一个通用的、多态函数包装器,用于存储可调用对象。函数指针、仿函数、 lambda 等可调⽤对象的类型各不相同,<font style="color:rgb(31,35,41);">std::function</font>的优势就是统⼀类型,对他们都可以进⾏包装,这样在很多地⽅就⽅便声明可调⽤对象的类型。

<font style="color:rgb(31,35,41);">std::function</font> 的实例对象可以包装存储其他的可以调⽤对象,包括函数指针、仿函数、 lambda 、 bind 表达式等,存储的可调⽤对象被称为 std::function 的⽬标。若 std::function 不含⽬标,则称它空。调空则抛出 std::bad_function_call 异常。


<font style="color:rgb(31,35,41);">function</font>被定义<font style="color:rgb(31,35,41);"><functional></font>头⽂件中:

std::function 的基本语法

#include <functional>template <class T>
class function; // 未定义的模板类template <class Ret, class... Args>
class function<Ret(Args...)>; // 以返回类型和参数类型列表定义模板
function<返回类型(可调用对象的参数类型1,参数类型2...)>  对象 = 可调用对象;
// int add(int a, int b)
// function<int(int, int)> func1 = add;

使用 std::function 包装不同的可调用对象

以下示例展示了 std::function 包装普通函数、仿函数、lambda 表达式、类静态成员函数和普通成员函数的用法。

#include<functional>
#include<iostream>
using namespace std;int f(int a, int b)
{return a + b;
}struct Functor
{
public:int operator() (int a, int b){return a + b;}};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 = 0;
};int main()
{// 包装各种可调⽤对象function<int(int, int)> f1 = f; // 普通函数function<int(int, int)> f2 = Functor(); // 仿函数function<int(int, int)> f3 = [](int a, int b) {return a + b; }; // lambdacout << f1(1, 1) << endl;cout << f2(1, 1) << endl;cout << f3(1, 1) << endl;// 包装静态成员函数// 成员函数要指定类域并且前⾯加&才能获取地址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;function<double(Plus, double, double)> f6 = &Plus::plusd;cout << f6(pd, 1.1, 1.1) << endl;cout << f6(pd, 1.1, 1.1) << endl;function<double(Plus&&, double, double)> f7 = &Plus::plusd;cout << f7(move(pd), 1.1, 1.1) << endl;cout << f7(Plus(), 1.1, 1.1) << endl;return 0;
}

在C++中,普通成员函数的调用与静态成员函数或普通的非成员函数不同,因为它隐含了一个 this 指针参数。这是由于普通成员函数总是绑定到某个对象实例,因此在调用时需要知道具体是哪个对象调用了该函数。

function包装普通成员函数为什么要传入 this 指针参数?

当我们使用 std::function 来包装普通成员函数时,普通成员函数的签名实际上是:

ReturnType (ClassType::*)(ParamTypes...)

这个签名表示该成员函数属于特定的类,因此它并不完全等同于普通函数。每个普通成员函数的调用实际上是通过一个特定的对象调用的,而对象的地址(this 指针)在函数调用时必须传入。

在普通成员函数的调用中:

  • this 指针作为隐式参数,指向调用函数的对象实例。
  • std::function 包装这种成员函数时需要显式地传入 this 指针,以便知道调用时该成员函数应该作用于哪个对象实例。

例如,假设有如下成员函数:

double Plus::plusd(double a, double b) {return a + b;
}

在使用 std::function 包装时,由于 plusd 是非静态成员函数,需要显式传入一个 Plus 实例(对象)或该实例的指针作为 this。可以通过传入对象指针 Plus*,或者直接传递一个对象实例 Plus 来间接实现这种绑定。

传入对象指针与传入对象实例的区别

  1. 传入对象指针(例如 Plus*:这种情况下,std::function 会调用成员函数时使用传入的指针来绑定 this。先创建一个Plus实例,然后传入该实例的地址。
function<double(Plus*, double, double)> f5 = &Plus::plusd;
Plus pd;
f5(&pd, 1.1, 1.1);

在这里,f5(&pd, 1.1, 1.1); 调用时,&pd 指向的对象作为 this 指针传入。

  1. 传入对象实例(例如 Plus:当传入一个对象时,C++ 会复制这个对象并为其分配一个独立的内存空间,然后将其临时地址传给 this,使得 this 指向该副本。
function<double(Plus, double, double)> f6 = &Plus::plusd;
Plus pd;
f6(pd, 1.1, 1.1);

这样,调用 f6(pd, 1.1, 1.1); 会将对象 pd 复制一份传入,使得成员函数 plusdthis 指针指向该副本。

传入对象实例的优缺点:

  • 优点:传入对象实例更加直观,代码上不需要关注指针。
  • 缺点:会产生对象的拷贝(除非对象使用 std::move),因此可能有额外的开销。如果对象较大或者包含较多成员变量,拷贝代价较高。
  1. **直接传入一个匿名对象:**减少拷贝次数
function<double(Plus, double, double)> f6 = &Plus::plusd;
f6(Plus(), 1.1, 1.1);

当我们使用 Plus() 作为匿名对象传入时:

  • 匿名对象在调用的那一行直接生成,不需要从其他地方复制数据。
  • std::function 中的 f6 会直接使用这个匿名对象,临时对象的生命周期刚好覆盖整个调用过程,调用结束后立即销毁。

这样做可以在保证代码清晰的同时避免多余的拷贝。所以,传入 Plus() 是一种优化写法,尤其适合对象初始化开销较大、但不需要持续存在的情况。

所以在包装匿名对象时一般推荐使用该种方法。

例题 :150. 逆波兰表达式求值 - ⼒扣(LeetCode)

. - 力扣(LeetCode)

// 使⽤map映射string和function的⽅式实现
// 这种⽅式的最⼤优势之⼀是⽅便扩展,假设还有其他运算,我们增加map中的映射即可
class Solution {
public:int evalRPN(vector<string>& tokens) {stack<int> st;// Fixing the map initializationmap<string, function<int(int, int)>> opFuncMap = {{"+", [](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(opFuncMap.count(str)){int top = st.top();st.pop();int next_top = st.top();st.pop();int ret = opFuncMap[str](next_top, top);st.push(ret);}else // Handling operand case{st.push(stoi(str));}}return st.top();}
};

<font style="color:rgb(31,35,41);">bind</font>

bind<functional>头文件中,std::bindfunction类似,也是⼀个函数模板,同时是一个函数适配器,用于将可调用对象的参数进行绑定或者参数顺序的调整,返回一个新的可调用对象(本质是一个仿函数对象)。

std::bind 可以调整原有函数的参数个数和顺序,适配更为灵活的调用方式。它广泛用于实现函数的“占位符”特性和简化代码的参数传递。

#include <functional>template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);

std::bind 的基本语法

auto newCallable = bind(callable,arg_list); 
// newCallable 为绑定后的可调用对象 类型由 auto 推导
// callable 是要进行绑定或者进行调整参数传递顺序的函数
// arg_list 是 callable 进行具体调整的参数列表(可以包括占位符或绑死的参数)

其中<font style="color:rgb(31,35,41);">newCallable</font>本⾝是⼀个可调⽤对象,<font style="color:rgb(31,35,41);">arg_list</font>是⼀个逗号分隔的参数列表,对应给定的<font style="color:rgb(31,35,41);">callable</font>的参数。当我们调⽤<font style="color:rgb(31,35,41);">newCallable</font>时,<font style="color:rgb(31,35,41);">newCallable</font>会调⽤<font style="color:rgb(31,35,41);">callable</font>,并传给它<font style="color:rgb(31,35,41);">arg_list</font>中的参数。

std::bind 参数的顺序调整与绑定

顺序调整

std::bind 中,通过 placeholders 命名空间可以使用 _1_2 等占位符表示绑定的函数参数。

using namespace placeholders; // 将占位符全部展开

这些占位符用于定义生成的可调用对象中参数的位置,例如 _1 表示第一个参数, _2 表示第二个参数,以此类推。

using placeholders::_1;
using placeholders::_2;
// using placeholders::_3;int Sub(int a, int b)
{return (a - b) * 10;
}auto sub1 = bind(Sub, _1, _2); 
// 传入Sub函数,_1 _2表示使用新的可调用对象sub1时传入的第一个和第二个参数
cout << sub1(10, 5) << endl; // Sub(10, 5);auto sub2 = bind(Sub, _2, _1); 
// 传入Sub函数,_1 _2表示使用新的可调用对象sub1时传入的第二个和第一个参数
cout << sub2(10, 5) << endl; // Sub(5, 10);

参数的绑定

如果想让某个参数的值进行绑定,就在该参数位置上传入值即可,之后如果有传入参数需要可以继续按照占位符当前个数继续进行填写。 **_1, _2 ...**仅表示绑定后的新可调用对象传入的参数及顺序。

// 调整参数个数 (常⽤)
auto sub3 = bind(Sub, 100, _1);
cout << sub3(5) << endl;auto sub4 = bind(Sub, _1, 100);
cout << sub4(5) << endl;// 分别绑死第1 2 3个参数
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;// function<返回值类型(传入的各个参数类型 ,...)>
// 成员函数对象进⾏绑死,就不需要每次都传递了
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需要包装的函数,将Plus::plusd函数包装后要传入的this部分绑死
function<double(double, double)> f7 = bind(&Plus::plusd, Plus(), _1, _2);
cout << f7(1.1, 1.1) << endl;

std::functionstd::bind 的实际应用

  1. 函数指针和回调函数

std::functionstd::bind 的组合可以让回调函数的参数更具灵活性。例如,在实现事件回调时可以使用 std::function 存储回调函数,并用 std::bind 将具体参数与回调绑定。

  1. 函数作为容器的元素

在需要存储不同类型的可调用对象的容器中,使用 std::function 是一个最佳选择。利用 std::function 可以将不同类型的函数包装在一个容器中统一存储,并在需要时调用。

  1. 参数绑定和延迟调用

std::bind 可以用于创建参数部分固定的函数对象,从而减少函数调用时的参数传递。这种方式在处理回调和异步编程中非常有用。


结论

C++11 提供的 std::functionstd::bind 为现代 C++ 编程带来了极大的便利。std::function 允许将不同类型的可调用对象进行统一存储和操作,简化了代码结构。而 std::bind 则可以灵活地调整函数参数和调用方式,为开发者提供了高效、简洁的代码编写方式。在日常开发中,合理运用这两个包装器可以显著提高代码的可读性和可维护性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/886053.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

企业一站式管理系统odoo的研究——系统搭建

大纲 1. 环境准备1.1 安装操作系统1.2 更新操作系统1.3 配置用户组和用户1.3.1 创建用户组 odoo1.3.2. 创建用户 odoo1.3.3. 设置用户 odoo 的密码1.3.4. 验证用户和组1.3.5. 将用户 odoo 添加到添加sudo组&#xff1a;1.3.6. 切到odoo用户 2. 安装 Odoo1. 安装依赖项目2.2. 安…

今天给在家介绍一篇基于jsp的旅游网站设计与实现

项目描述 临近学期结束&#xff0c;还是毕业设计&#xff0c;你还在做java程序网络编程&#xff0c;期末作业&#xff0c;老师的作业要求觉得大了吗?不知道毕业设计该怎么办?网页功能的数量是否太多?没有合适的类型或系统?等等。这里根据疫情当下&#xff0c;你想解决的问…

SMA-BP基于黏菌算法优化BP神经网络时间序列预测

项目源码获取方式见文章末尾&#xff01; 600多个深度学习项目资料&#xff0c;快来加入社群一起学习吧。 《------往期经典推荐------》 项目名称 1.【基于CNN-RNN的影像报告生成】 2.【卫星图像道路检测DeepLabV3Plus模型】 3.【GAN模型实现二次元头像生成】 4.【CNN模型实现…

vue3+ts+antd 运行报错 convertLegacyToken is not a function

以上代码报错 在github上看到有将 const v3Token convertLegacyToken(mapToken); 改成 const v3Token convertLegacyToken.default(mapToken);运行时就不报错了 但是到了打包的时候还是报错 但是ctrl点击convertLegacyToken能够正常跳转过去 于是我打印convertLegacyToken 发…

StarRocks Summit Asia 2024 全部议程公布!

随着企业数字化转型深入&#xff0c;云原生架构正成为湖仓部署的新标准。弹性扩展、资源隔离、成本优化&#xff0c;帮助企业在云上获得了更高的灵活性和效率。与此同时&#xff0c;云原生架构也为湖仓与 AI 的深度融合奠定了基础。 在过去一年&#xff0c;湖仓技术与 AI 的结…

HTML之列表学习记录

练习题&#xff1a; 图所示为一个问卷调查网页&#xff0c;请制作出来。要求&#xff1a;大标题用h1标签&#xff1b;小题目用h3标签&#xff1b;前两个问题使用有序列表&#xff1b;最后一个问题使用无序列表。 代码&#xff1a; <!DOCTYPE html> <html> <he…

掌控板micropython编程实现网页实时显示板载光线传感器的值

掌控板micropython编程实现网页实时显示板载光线传感器的值 一、AJAX简介 AJAX&#xff08;Asynchronous JavaScript and XML&#xff09;是一种在无需重新加载整个页面的情况下&#xff0c;能够更新部分网页内容的技术。它允许Web页面与服务器进行异步数据交换&#xff0c;这…

Linux 常用操作指令大揭秘(下)

&#x1f31f;快来参与讨论&#x1f4ac;&#xff0c;点赞&#x1f44d;、收藏⭐、分享&#x1f4e4;&#xff0c;共创活力社区。 &#x1f31f; &#x1f6a9;用通俗易懂且不失专业性的文字&#xff0c;讲解计算机领域那些看似枯燥的知识点&#x1f6a9; 目录 &#x1f4af;…

Spring源码(十二):Spring MVC之Spring Boot

本篇将详细讨论Spring Boot 的启动/加载、处理请求的具体流程。我们先从一个简单的Spring Boot项目日志开始分析&#xff08;这里假设读者已经仔细阅读完了前面的文章&#xff0c;且对Spring源码有一定深度的了解&#xff0c;否则会看得一脸懵逼&#xff09;。 本文为2024重置…

【C语言刷力扣】13.罗马数字转整数

题目&#xff1a; 解题思路: 倒序遍历&#xff0c;若当前字符代表的数字比上一字符代表的数字小&#xff0c;即减去当前字符数字。 时间复杂度&#xff1a; 空间复杂度&#xff1a; int romanToInt(char* s) {int ans 0;int low 0;int num[26];num[I - A] 1;num[V - A]…

【Unity Bug 随记】unity version control 报 xx is not in a workspace.

可能原因是更改了仓库或者项目名称。 解决办法就是重置Unity Version Control&#xff0c;去Hub disconnect 然后重新connect cloud和UVC UVC可能连不上&#xff0c;直接进入项目就行&#xff0c;打开版本管理标签会让你重新连工作区&#xff0c;选择你的仓库和工作区 然后In…

springboot读取modbus数据

1、引入依赖 jlibmodbus <dependency><groupId>com.intelligt.modbus</groupId><artifactId>jlibmodbus</artifactId><version>1.2.9.7</version> </dependency> 2、数据获取 public String processData(String ip) {tr…

LabVIEW中坐标排序与旋转 参见附件snippet程序

LabVIEW中坐标排序与旋转 参见附件snippet程序LabVIEW中坐标排序与旋转 参见附件snippet程序 - 北京瀚文网星科技有限公司 在LabVIEW中处理坐标排序的过程&#xff0c;尤其是按顺时针或逆时针排列坐标点&#xff0c;常见的应用包括处理几何形状、路径规划等任务。下面我将为您…

51单片机应用开发(进阶)---定时器应用(电子时钟)

实现目标 1、巩固定时器的配置流程&#xff1b; 2、掌握按键、数码管与定时器配合使用&#xff1b; 3、功能1&#xff1a;&#xff08;1&#xff09;简单显示时间。显示格式&#xff1a;88-88-88&#xff08;时-分-秒&#xff09; 4、功能2&#xff1a;&#xff08;1&#…

FPGA实现PCIE采集电脑端视频转SFP光口万兆UDP输出,基于XDMA+GTX架构,提供2套工程源码和技术支持

目录 1、前言工程概述免责声明 2、相关方案推荐我已有的PCIE方案10G Ethernet Subsystem实现万兆以太网物理层方案 3、PCIE基础知识扫描4、工程详细设计方案工程设计原理框图电脑端视频PCIE视频采集QT上位机XDMA配置及使用XDMA中断模块FDMA图像缓存UDP视频组包发送UDP协议栈MAC…

使用 unicorn 和 capstone 库来模拟 ARM Thumb 指令的执行(一)

import binascii import unicorn import capstonedef printArm32Regs(mu):for i in range(66,78):print("R%d,value:%x"%(i-66,mu.reg_read(i)))def testhumb():CODE b\x1C\x00\x0A\x46\x1E\x00"""MOV R3, R0 的机器码&#xff1a;0x1C 0x00&#xf…

git重置的四种类型(Git Reset)

git区域概念 1.工作区:IDEA中红色显示文件为工作区中的文件 (还未使用git add命令加入暂存区) 2.暂存区:IDEA中绿色(本次还未提交的新增的文件显示为绿色)或者蓝色(本次修改的之前版本提交的文件但本次还未提交的文件显示为蓝色)显示的文件为暂存区中的文件&#xff08;使用了…

第三十一天|贪心算法| 56. 合并区间,738.单调递增的数字 , 968.监控二叉树

目录 56. 合并区间 方法1&#xff1a;fff 看方法2&#xff1a;fff优化版 方法3&#xff1a; 738.单调递增的数字 968.监控二叉树&#xff08;贪心二叉树&#xff09; 56. 合并区间 判断重叠区间问题&#xff0c;与452和435是一个套路 方法1&#xff1a;fff 看方法2&am…

LeetCode 热题100(八)【二叉树】(3)

目录 8.11二叉树展开为链表&#xff08;中等&#xff09; 8.12从前序与中序遍历序列构造二叉树&#xff08;中等&#xff09; 8.13路径总和III&#xff08;中等&#xff09; 8.14二叉树的最近公共祖先&#xff08;中等&#xff09; 8.15二叉树中的最大路径和&#xff08;困…

AutoSAR CP DoIP规范导读

主要功能和用途 诊断通信协议实现 遵循标准&#xff1a;遵循ISO 13400 - 2标准&#xff0c;实现了诊断通信在IP网络上的传输协议和网络层服务&#xff0c;包括数据封装、传输、路由等功能。 多种消息支持 车辆识别与公告&#xff1a;能够进行车辆识别请求和响应&#xff0c;…