前言
如果你还不知道 C++ 11 引入的包装器和绑定器是什么,可以读读这篇文章,看看有什么 启发;如果你已经对包装器和绑定器了如指掌,也可以读读这篇文章,看看有什么 补充。欢迎交流~😏
可调用对象
C++中存在【可调用对象】这样一个概念。即对于一个对象或一个表达式,如果可以对其使用调用运算符()
,则称它为可调用对象。
一般来说,可调用对象归类为以下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::_1
和std::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;
}
- 在上述代码中,我们在类AA中定义了一个可调用对象
m_callback
作为回调函数对象,它接收一个const string类对象的引用,返回void。
function<void(const string&)> m_callback; //回调函数对象
- 接着定义一个变长模板函数
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。
最后😆
本文部分参考自文章。