【C++11】三大神器之——包装器和绑定器

news2024/11/22 22:05:16

前言

如果你还不知道 C++ 11 引入的包装器和绑定器是什么,可以读读这篇文章,看看有什么 启发;如果你已经对包装器和绑定器了如指掌,也可以读读这篇文章,看看有什么 补充。欢迎交流~😏

可调用对象

C++中存在【可调用对象】这样一个概念。即对于一个对象或一个表达式,如果可以对其使用调用运算符(),则称它为可调用对象

一般来说,可调用对象归类为以下6种:

  1. 普通的函数
  2. 函数指针
  3. 仿函数
  4. 类的静态成员函数
  5. 类的非静态成员函数
  6. lambda表达式

接下来我们用几个示例来展示它们的调用:

函数指针

普通函数没什么好说的,它是我们最常见的可调用对象。

//普通的函数
void func(const string& str)
{
    cout << str << endl; 
}

我们知道函数也有地址,那么我们可以通过定义一个函数指针来绑定它,示例如下:

void func(const string& str)
{
    cout << str << endl; 
}
int main()
{
	//定义普通函数func的函数指针fp1
	void(*fp1) (const string&) = &func; 
	//调用函数指针
	fp1("我是由函数指针调用的普通函数");
	return 0;
}

仿函数

仿函数其实就是对类对象重载了operator()的运算符:

class myclass1{
public:
    void operator()(const string& str)
    {   
        cout << str << endl; 
    }
};

调用仿函数的方法多样:

//调用仿函数
int main()
{
	//先实例一个类对象
	myclass1 aa;
	//通过隐式调用
	aa("我是由仿函数对象调用的函数");
	//通过显示调用
    aa.operator()("我是由仿函数对象调用的函数");
    //通过临时对象调用
    myclass1{}.operator()("我是由仿函数对象调用的函数");
	return 0;
}

类的静态成员函数

类的静态成员函数和普通函数本质上是一样的,把普通函数放在类中而已:

class myclass2{
public:
    static void func(const string& str)
    {
        cout << str << endl; 
    }
};
int main()
{
 	myclass2 cc;
    cc.func("我是类的静态成员函数");
	return 0;
}

我们同样可以使用函数指针调用类的静态成员函数,只需要指出其所属的类即可:

	void(*fp4)(const string& ) = myclass2::func; 
	fp4("我是函数指针调用类的静态成员函数");

Lambda表达式(匿名函数)

接下来的两个可调用对象比较特殊,lambda表达式是C++11新加入的特性,它的语法如下:
在这里插入图片描述
[captures] (params) -> return_type { statments;}

我们可以用同为C++11新特性的自动推导类型auto将lambda表达式拷贝到名为f的可调用对象中:

    auto f = [](const string& str)
    {
        cout << str << endl; 
    };
    f("我是lambda函数");

类的非静态成员函数

类的非静态成员函数有地址,但是只能通过类的对象才能调用它,所以C++对它做了特别处理:

class myclass3{
public:
    void func(const string& str)
    {
        cout << str << endl; 
    }
};
int main()
{
	//同样需要实例化对象bb
	myclass2 bb;
	//我们可以通过函数指针调用类的非静态成员函数
    void(myclass2::*fp3) (const string& ) = &myclass2::func; 
    //调用的方法有区别
    (bb.*fp3)("我是由函数指针调用的类的非静态成员函数");
	return 0;
}

在上面的例子中满足条件的这些可调用对象对应的类型被统称为可调用类型。

C++ 中的可调用类型虽然具有比较统一的操作形式,但定义的方式五花八门。如果我们试图使用统一的方式保存,或者传递一个可调用对象时会十分繁琐。

现在,C++11通过提供包装器std::function绑定器std::bind统一了可调用对象的各种操作。

包装器

std::function类模板函数是一个通用的可调用对象的包装器,它用简单的、统一的方式处理可调用对象。使用包装器之前需要包含头文件:#include <functional>

它的格式是:function<返回类型(参数列表)>

如果std::function对象未包装可调用对象,使用std::function对象将抛出std::bad_function_call异常。

接下来我们用包装器来包装上文中的可调用对象:

#include <iostream>
#include <functional>
#include <string>
using namespace std;

//1、普通的函数
void func(const string& str)
{
    cout << str << endl; 
}

//2、仿函数
class myclass1{
public:
    void operator()(const string& str)
    {   
        cout << str << endl; 
    }
};

//3、类中普通成员函数
class myclass2{
public:
    void func(const string& str)
    {
        cout << str << endl; 
    }
};

//4、类中静态成员函数
class myclass3{
public:
    static void func(const string& str)
    {
        cout << str << endl; 
    }
};

int main()
{
    //普通函数
    //由函数指针调用
    void(*fp1) (const string& ) = func;
    fp1("我是由函数指针调用的普通函数");
    //用包装器调用
    function<void(const string&)> ff1 = func;
    ff1("我是由包装器调用的普通函数");

    //仿函数
    myclass1 aa;
    aa("我是由仿函数对象调用的函数");
    // aa.operator()("我是由仿函数对象调用的函数");
    // myclass1{}.operator()("我是由仿函数对象调用的函数");
   //用包装器调用
    function<void(const string&)> ff2 = myclass1();
    ff2("我是由包装器调用的仿函数");

    //用函数指针调用的类的非静态成员函数
    myclass2 bb;
    void(myclass2::*fp3) (const string& ) = &myclass2::func; 
    (bb.*fp3)("我是由函数指针调用的类的非静态成员函数");
    //用包装器调用,传入类名
    function<void(myclass2, const string&)> ff3 = &myclass2::func;
    //需要传入this指针
    ff3(bb, "我是由包装器调用类的静态非成员函数");

    //用函数指针调用类的静态成员函数
    myclass3 cc;
    // cc.func("我是函数指针调用类的静态成员函数");
    void(*fp4)(const string& ) = myclass3::func; 
    fp4("我是函数指针调用类的静态成员函数");
    //用包装器调用
    function<void(const string&)> ff4 = myclass3::func;
    ff4("我是由包装器调用类的静态成员函数");

    //匿名函数
    auto f = [](const string& str)
    {
        cout << str << endl; 
    };
    f("我是lambda函数");
    //用包装器调用
    function<void(const string&)> ff5 = f;			
	ff5("我是由包装器调用的lambda函数");										

    return 0;
}

通过上述示例代码,我们可以发现:

  • 除了类的非静态成员函数,其他的可调用对象通过包装器的包装,得到了一个统一的格式,包装完成得到的对象相当于一个函数指针,和函数指针的使用方式相同。
  • 类的非静态成员函数还需要传入this指针,所以单独使用std::function是不够的,还需要结合使用std::bind函数绑定this指针以及参数列表。

绑定器

std::bind()模板函数是一个通用的函数适配器(绑定器),它用一个可调用对象及其参数,生成一个新的可调用对象,以适应模板。std::bind()返回std::function的对象。

我们先来看绑定器的基本用法:

有这样一个普通的函数func,我们用绑定器将它和它的参数绑定在一起,返回一个新的可调用对象。

void func(int i, const string& str)
{
    while(i--)
    {
        cout << str << endl;
    }
    cout << endl;
}
int main()
{
	function<void(int, const string&)> fn1 = 
	//bind(可调用对象,参数列表)
	bind(func, placeholders::_1,placeholders::_2);//placeholders::_1/_2是参数占位符
	fn1(2, "普通函数");
	return 0;
}

在使用std::bind绑定类成员函数的时候需要注意绑定参数的顺序

  • std::placeholders::_1std::placeholders::_2是参数占位符,表示传入的一个参数和第二参数。

假如现在要求交换func形参的顺序void func( const string& str, int ),一般需要函数重载。但是现在用bind可以解决这个问题:我们只需将占位符1,和占位符2的位置调换

    //如果需要交换形参的顺序,一般需要函数重载,但用bind可以解决这个问题
    function<void(const string&, int)> fn2 = 
    bind(func, placeholders::_2, placeholders::_1);    								
    fn2("普通函数", 2);

现在我们使用绑定器将类的非静态成员函数统一格式:

//1、普通的函数
void func(int i, const string& str)
{
    while(i--)
    {
        cout << str << endl;
    }
    cout << endl;
}

//2、仿函数
class myclass1{
public:
    void operator()(int i, const string& str)
    {   
        while(i--)
        {
            cout << str << endl;
        }
        cout << endl;
    }
};

//3、类中普通成员函数
class myclass2{
public:
    void func(int i, const string& str)
    {
        while(i--)
        {
            cout << str << endl;
        }
        cout << endl;
    }
};

//4、类中静态成员函数
class myclass3{
public:
    static void func(int i, const string& str)
    {
        while(i--)
        {
            cout << str << endl;
        }
        cout << endl;
    }
};

int main()
{
    // 绑定普通函数 placeholders::_1是函数的第一个形参,placeholders::_2是第二个形参
    function<void(int, const string&)> fn1 = 
    bind(func, placeholders::_1, placeholders::_2);		
    fn1(2, "普通函数");
    
    // 类的静态成员函数
    function<void(int, const string&)> fn3 = 
    bind(myclass3::func, placeholders::_1, placeholders::_2);
    fn3(2, "类的静态成员函数");

    // 仿函数
    function<void(int, const string&)> fn4 = 
    bind(myclass1(), placeholders::_1, placeholders::_2);
    fn4(2, "仿函数");

    // 创建lambda对象
	auto lb = [](int i, const string& str) {
		while(i--)
        {
            cout << str << endl;
        }
        cout << endl;
	};
	function<void(int, const string&)> fn5 = 
	bind(lb, placeholders::_1, placeholders::_2);	
    fn5(2, "创建lambda对象");

    // 类的非静态成员函数,这样一来就可以通过模板的方式使用类的非静态成员函数的调用了
	myclass2 aa;
    function<void(int, const string&)> fn6 = 
    bind(&myclass2::func,&aa,placeholders::_1, placeholders::_2);
    fn6(2, "类的非静态成员函数");
    
    return 0;
}

对绑定类的非静态成员函数:

  • 第一个参数为类成员函数名的引用(推荐使用引用),
  • 第二个参数为this指针上下文,即特定的对象实例,
  • 之后的参数分别指定类成员函数的第1,2,3依次的参数值。

我们依旧可以通过auto接收绑定器std::bind返回的可调用对象,这样使用起来会更容易一些。

回调函数

什么是回调函数?

来自Stack Overflow某位大神简洁明了的表述:A “callback” is any function that is called by another function which takes the first function as a parameter。

也就是说,函数F1调用函数F2的时候,函数F1通过参数给函数F2传递了另外一个函数 F3的指针,在函数F2执行的过程中,函数F2调用了函数F3,这个动作就叫做回调(Callback),而先被当做指针传入、后面又被回调的函数F3就是回调函数。

在这里插入图片描述

为什么需要回调函数?

乍一看,回调似乎只是函数间的调用,和普通函数调用没啥区别。但仔细一看,可以发现两者之间的一个关键的不同:
在回调中,主程序F1把回调函数像参数一样传入库函数F2。这样一来,只要我们改变传进库函数的参数,就可以实现不同的功能,这样有没有觉得很灵活?并且丝毫不需要修改库函数的实现,这就是解耦。
再仔细看看,主函数和回调函数是在同一层的,而库函数在另外一层,如果库函数对我们不可见,我们修改不了库函数的实现,也就是说不能通过修改库函数让库函数调用普通函数那样实现,那我们就只能通过传入不同的回调函数了,这也就是在日常工作中常见的情况。

在如下的示例代码中,我们将利用包装器和绑定器实现回调函数:

阅读本节需要读者有一定的模板编程和多线程编程的基础,关于模板部分可以参考我之前的文章。

#include <iostream>
#include <string>
#include <thread>                      // 线程类头文件。
#include <mutex>                      // 互斥锁类的头文件。
#include <deque>                      // deque容器的头文件。
#include <queue>                      // queue容器的头文件。
#include <condition_variable>  // 条件变量的头文件。
#include <functional>
using namespace std;

//回调函数

//处理数据的函数 其他可调用对象与之相同
void solve(const string& str)
{
    cout << "处理数据:" << str << endl;
}

//处理数据的类
struct BB
{
    void solve(const string& str)
    {
        cout << "处理数据:" << str << endl;
    }
};

//实际工程中,AA类的框架不能更改,需要回调函数
class AA{
    mutex m_mtx;  //互斥锁
    condition_variable m_cond;  //条件变量
    queue<string, deque<string>> m_q;   //缓存队列
    function<void(const string&)> m_callback;   //回调函数对象
public:
    //注册回调函数,该回调函数只有一个参数即消费者接收的数据message
    template<typename Fn, typename...Args>
    void callback(Fn&& fn, Args&& ...args)
    {
        //如果传进来的可调用对象是类的成员函数,那么可变参数包会展开成对象的this指针,
        //如果传进来的可调用对象是除类的成员函数以外的,那么可变参数包就是空的
        //第三个参数是一个占位符,传入的是接收的数据message
        m_callback = bind(forward<Fn>(fn), 
        forward<Args>(args)..., std::placeholders::_1);
    }

    //生产数据
    void incache(int num)   //指定生产num个数据
    {
        lock_guard<mutex> mlock(m_mtx);//申请加锁
        for(int i = 0; i < num; i++)
        {
            static int number = 1;
            string data = to_string(number++) + "号数据";
            m_q.push(data);
        }
        // m_cond.notify_one();    //唤醒一个被当前条件变量阻塞的线程
        m_cond.notify_all();    //唤醒所有被当前条件变量阻塞的线程
    }

    //消费数据
    void outcache()
    {
        while(true)
        {
            string message;
            {
                // 把互斥锁转换成unique_lock<mutex>,并申请加锁
                unique_lock<mutex> lock(m_mtx);

                //如果缓存队列是空的(生产者没有生产多的数据),消费者进程就阻塞(等待),直到被唤醒
                //wait()执行:1)把互斥锁解开;2)阻塞,等待被唤醒;3)给互斥锁加锁
                while(m_q.empty()) m_cond.wait(lock);

                //数据元素出队
                message = m_q.front();
                m_q.pop();

                cout << "线程:" << this_thread::get_id() << "," << message << endl;
            }
            //这里unique_lock自动解锁

            // 处理出队的数据(把数据消费掉)。
            if(m_callback)m_callback(message);  //回调函数 
        }  
    }
};

int main()
{
    AA aa;

    //在创建线程之前,先注册回调函数
    //如果是非类的成员函数
    // aa.callback(solve);

    //如果是类的成员函数
    BB bb;
    aa.callback(&BB::solve, &bb);

    //创建消费者线程
    thread t1(&AA::outcache, &aa);
    thread t2(&AA::outcache, &aa);
    thread t3(&AA::outcache, &aa);

    this_thread::sleep_for(chrono::seconds(2));    // 休眠2秒

    //生产者开始生产3个数据
    aa.incache(3);

    this_thread::sleep_for(chrono::seconds(3));    // 休眠3秒

    //生产者开始生产5个数据
    aa.incache(5);

    t1.join();
    t2.join();
    t3.join();
    
    system("pause");
    return 0;
}
  1. 在上述代码中,我们在类AA中定义了一个可调用对象m_callback作为回调函数对象,它接收一个const string类对象的引用,返回void。
  function<void(const string&)> m_callback;   //回调函数对象
  1. 接着定义一个变长模板函数callback用于注册回调函数。
	template<typename Fn, typename...Args>
	  void callback(Fn&& fn, Args&& ...args)
	   {
	     m_callback = 
	     bind(forward<Fn>(fn), forward<Args>(args)..., std::placeholders::_1);
	   }
  • 对于这个变长模板,如果传进来的可调用对象是类的成员函数,那么可变参数包args会展开成对象的this指针,
  • 如果传进来的可调用对象是除类的成员函数以外的,那么可变参数包args就是空的,
  • 第三个参数是一个占位符,传入的是接收的数据message。

在实际工程中,由于AA类的框架不能更改,所以需要回调函数:

int main()
{
	...
	//如果是非类的成员函数
    // aa.callback(solve);
	BB bb;//处理数据的类
    aa.callback(&BB::solve, &bb);
    ...
}

在main函数中,通过调用AA::callback()注册回调函数m_callback,回调函数与处理数据的类方法BB::solve()绑定,接收数据message。

最后😆

本文部分参考自文章。

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

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

相关文章

PCB叠层当中的“假八层”是什么意思呢?

大家在进行PCB设计的时候都是需要对我们的板子选择叠层方案的&#xff0c;一个好的层叠方案能使我们的信号质量变好&#xff0c;板子性能也会更稳定等等&#xff0c;大家可能或多或少的接触过多层板&#xff0c;也就是两层往上的板子&#xff0c;那么大家在做六层板的时候是否有…

Servlet:狂神Response源码分析【文件下载 + 动态图形验证码 + 重定向】

目录web.xmlindex.jspRequestTestlogin测试FileServlet文件下载测试动态图形验证码Servlet动态图形验证码测试总结web.xml <?xml version"1.0" encoding"UTF-8"?> <web-app xmlns"http://xmlns.jcp.org/xml/ns/javaee"xmlns:xsi&quo…

15-16-17 - 保护模式中的特权级

---- 整理自狄泰软件唐佐林老师课程 文章目录1. 保护模式小结1.1 使用选择子访问段描述符表时&#xff0c;索引值的合法性检测1.2 内存段类型合法性检测1.3 实例分析2. 问题一3. 保护模式中的特权级3.1 特权级的表现形式3.2 初探特权级3.2.1 CPL和DPL的关系3.2.2 段描述符中的D…

手把手教你Spring Cloud Alibaba教程:使用nacos实现服务注册与发现

我们在上一篇&#xff1a;手把手教你Spring Cloud Alibaba教程:nacos安装 接下来我们来实现下基本的服务注册和发现 版本确认 我们需要确认spring Cloud Alibaba version对应的nacso version 主要如下 版本说明 alibaba/spring-cloud-alibaba Wiki Spring Cloud Alibaba Ve…

图解:基于HyperWorks螺纹升角的六面体划分攻略

导读&#xff1a;在实际工程应用中&#xff0c;基于HyperWorks六面体工程命令&#xff0c;对工程师朋友来说比较繁琐&#xff0c;甚至不容易掌握。于是经常参加一些线下培训&#xff0c;用以提升自己业务技能。今天&#xff0c;笔者从一个企业资深培训讲师的角度&#xff0c;结…

Docker安装Oracle

Docker安装Oracle 本次使用的系统是centOS7 &#xff08;文章部分参考&#xff1a;地址&#xff09; 安装Docker docker安装要求&#xff1a;CentOS内核版本高于3.10&#xff0c;可以通过命令 uname -r查看当前内核版本 下载工具 yum install -y yum-utils设置阿里云镜像 …

【MySQL】使用C语言连接数据库

文章目录下载Mysql的C接口库程序中引入Mysql头文件和库文件**Mysql接口介绍**创建句柄链接数据库设置字符编码执行SQL语句关闭链接mysql.ops.cc下载Mysql的C接口库 要使用C语言连接mysql,需要使用mysql官网提供的库,大家可以官网下载,实际上连接数据库的功能在mysql 8.0版本之…

(十五)Vue之过滤器

文章目录计算属性实现methods实现过滤器实现局部过滤器不传参传参多个过滤器使用全局过滤器Vue学习目录 上一篇&#xff1a;&#xff08;十四&#xff09;Vue之收集表单数据 先看一个需求&#xff1a;给一个时间戳&#xff0c;然后把时间戳格式化显示出来 时间戳数据&#xf…

哈希表题目:相交链表

文章目录题目标题和出处难度题目描述要求示例数据范围进阶解法一思路和算法代码复杂度分析解法二思路和算法证明代码复杂度分析题目 标题和出处 标题&#xff1a;相交链表 出处&#xff1a;160. 相交链表 难度 2 级 题目描述 要求 给你两个单链表的头结点 headA\texttt…

web网页设计期末课程大作业 基于HTML+CSS仿苹果商城电商项目的设计与实现

常见网页设计作业题材有 个人、 美食、 公司、 学校、 旅游、 电商、 宠物、 电器、 茶叶、 家居、 酒店、 舞蹈、 动漫、 服装、 体育、 化妆品、 物流、 环保、 书籍、 婚纱、 游戏、 节日、 戒烟、 电影、 摄影、 文化、 家乡、 鲜花、 礼品、 汽车、 其他等网页设计题目, A…

Thread类和线程状态

目录 Thread 的常见构造方法 Thread 的几个常见属性 经典面试题&#xff1a;start和run的区别 interrupt 线程中断 join 线程等待 currentThread 获得当前这个线程对应的 Thread 对象的引用。 sleep 置顶休眠的时间(阻塞一会) sleep(1000)&#xff0c;真的只是休眠1000…

使用 MindStudio进行基于 MindX SDK 辅助驾驶项目的开发

视频链接&#xff1a;https://www.bilibili.com/video/BV1K8411p7Cm/ 1 任务介绍 ASSISTED-DRIVING的中文含义为辅助驾驶&#xff0c;其先采用了yolov3模型将图片中的交通标志检测出来&#xff0c;然后利用resnet18模型获对检测出来的交通标志进行分类&#xff1b;在道路线分…

0-1规划下的数独问题

数独显然是一个0-1规划问题.虽然这个什么凸分析还是啥的分析有这个优化的方法,但是,你DFS也不是完全一点不可能的对吧.嗯,既然这样的话,我们就要去解决这样一个更细致的问题了.数独这个问题他的解的存在性,唯一性怎么样.当然,已经有结论了,一个9*9的数独至少要给出17个值才能约…

Qt扫盲-QGridLayout理论总结

QGridLayout理论总结1. 简介2. 操作布局元素3. 常用属性4. 间隔设置1. 简介 QGridLayout 占用来自其父布局或 parentWidget()获得的空间&#xff0c;将其划分为行列表格形式&#xff0c;在每个布局的表格里放置控件或者布局器就行。 一般来说&#xff0c;列和行的行为功能相同…

Sourcetree安装详细步骤

前言&#xff1a; Sourxetree 作为 免费的 Git 客户端工具&#xff0c;有许多优点。Sourcetree 简化了与Git存储库交互的方式&#xff0c;因此我们可以专注于编码。通过 Sourcetree 简单又快捷的管理我们 的存储库。 下载安装包 进入 官网 选择Windows系统的安装包 &#xff…

【大数据技术Hadoop+Spark】HDFS Shell常用命令及HDFS Java API详解及实战(超详细 附源码)

需要源码请点赞关注收藏后评论区留言私信~~~ 一、HDFS的Shell介绍 Shell在计算机科学中俗称“壳”&#xff0c;是提供给使用者使用界面的进行与系统交互的软件&#xff0c;通过接收用户输入的命令执行相应的操作&#xff0c;Shell分为图形界面Shell和命令行式Shell。 文件系统…

PTC Creo Illustrate生产技术设备

PTC Creo Illustrate生产技术设备 Creo Illustrator是一款适用于生产技术设备的软件。该软件将三种功能与当前CAD设计数据、技术规范以及所需和可能使用的各种部件相结合。这些工具对供应商和客户尤其有用。供应商可以使用本文档中提供的信息熟悉机器。查看内部零件&#xff0c…

YOLO系列目标检测算法——YOLOR

YOLO系列目标检测算法目录 - 文章链接 YOLO系列目标检测算法总结对比- 文章链接 YOLOv1- 文章链接 YOLOv2- 文章链接 YOLOv3- 文章链接 YOLOv4- 文章链接 Scaled-YOLOv4- 文章链接 YOLOv5- 文章链接 YOLOv6- 文章链接 YOLOv7- 文章链接 PP-YOLO- 文章链接 …

喜讯 | 美格智能子公司美格智联成功获选2022年首批国家级“高新技术企业”认定

近日&#xff0c;全国高新技术企业认定管理工作领导小组办公室发布了关于对深圳市认定机构2022年认定的第一批高新技术企业进行备案公示的通知&#xff0c;根据《高新技术企业认定管理办法》&#xff08;国科发火〔2016〕32号&#xff09;和《高新技术企业认定管理工作指引》&a…

[C/C++/初学者]500以内的亲密数对(VS2012)

在开始编写程序之前&#xff0c;我们需要了解一个东西。 何为亲密数对&#xff1f; 简单来说&#xff0c;就是数a的正因子数&#xff08;除本身外&#xff09;等于数b的正因子数&#xff08;除本身外&#xff09;。 符合这项条件的两个数&#xff0c;我们称他们为亲密数对。 …