c++ 回调函数与std::function使用实例

理解回调


实际项目中,经常有使用回调函数的需求,如:

  • 双方通信中,等待接收对方的数据并处理,如使用socket进行的TCP通信
  • 定时器事件,当定时器计时结束时,需要处理某任务
  • 信号的触发,需要执行某任务

在同步编程中,需要启动一个专门的线程来阻塞监听并处理那些可能在未来发生的事件,而在异步编程中,只需要使用回调函数就可以了。

在这种场景下,可以简单地理解为:我为某事件注册一个处理函数,当该事件发生时,会自动调用该函数。该函数即可理解为回调函数。

其实,提供回调函数,也就是提供了处理函数的指针(地址),事件发生后,通过调用函数指针达到调用该函数的目的。

简单流程:

  • 定义一个普通函数,作为处理函数
  • 将处理函数的地址注册给调用者
  • 调用者在适当的时刻通过函数指针调用处理函数
方法

  1. c-style 函数指针

通过将回调函数的地址传给被调函数实现,如下:

void handle(char *s) // 处理函数
{
	printf("%s\n", s);
}

void (*pfoo) (char *); //函数指针,以char *为参数,返回void,与处理函数签名一致

// 简化函数指针类型的变量定义
typedef void(*pfoo)(char *);
// 以后就可以把pfoo类型的函数传递给需要pfoo参数的触发器
void event_trigger(pfoo p)
{
	p("event happened!");
}

int main(void)
{
	event_trigger(handle);
	
	return 0;
}
  1. std::function

通用多态函数包裹器,它能存储、复制、触发任何可调用对象,包括函数,Lambda表达式、bind表达式、函数指针等函数对象。

有关Lambda表达式的更多介绍,请参考c++中的Lambda表达式。

例子如下(出自cppreference.com):

#include 
#include 
 
struct Foo {
    Foo(int num) : num_(num) {}
    void print_add(int i) const { std::cout << num_+i << '\n'; }
    int num_;
};
 
void print_num(int i)
{
    std::cout << i << '\n';
}
 
struct PrintNum {
    void operator()(int i) const
    {
        std::cout << i << '\n';
    }
};
 
int main()
{
    // store a free function
    std::function<void(int)> f_display = print_num;
    f_display(-9);
 
    // store a lambda
    std::function<void()> f_display_42 = []() { print_num(42); };
    f_display_42();
 
    // store the result of a call to std::bind
    std::function<void()> f_display_31337 = std::bind(print_num, 31337);
    f_display_31337();
 
    // store a call to a member function
    std::function<void(const Foo&, int)> f_add_display = &Foo::print_add;
    const Foo foo(314159);
    f_add_display(foo, 1);
    f_add_display(314159, 1);
 
    // store a call to a data member accessor
    std::function<int(Foo const&)> f_num = &Foo::num_;
    std::cout << "num_: " << f_num(foo) << '\n';
 
    // store a call to a member function and object
    using std::placeholders::_1;
    std::function<void(int)> f_add_display2 = std::bind( &Foo::print_add, foo, _1 );
    f_add_display2(2);
 
    // store a call to a member function and object ptr
    std::function<void(int)> f_add_display3 = std::bind( &Foo::print_add, &foo, _1 );
    f_add_display3(3);
 
    // store a call to a function object
    std::function<void(int)> f_display_obj = PrintNum();
    f_display_obj(18);
}

输出如下:

-9
42
31337
314160
314160
num_: 314159
314161
314162
18

总之,std::function功能强大,使用方便,可以在使用中体会它的妙处。

多个类之间的交叉调用


经常会在这样的场景,类A中需要在类B的实例,而类B又需要调用A的接口,考虑如下场景:

某数据处理平台(DataMgr类实现)需要处理客户的发送的数据,TCP数据收发使用TcpMgr类实现,但数据的处理在 DataMgr 中完成。流程如下:

  1. DataMgr 通过 TcpMgr 收发数据
  2. TcpMgr 接收到数据后,通过 DataMgr 中函数处理数据

如下代码片断(省略其余部分):

// DataMgr.h文件

#include "TcpMgr.h"

class DataMgr 
{
public:
	void Init();
	void HandleData(const string &data)
	{
		// ...
	}
	
private:
	boost::shared_ptr<TcpMgr> m_pTcpMgr;
};

// TcpMgr.h文件
class TcpMgr
{
public:
	void OnRecvedData(const string &data)
	{
		// 调用DataMgr::HandleData
		// 如何调用?
	}
}

有多种方法可以实现上述调用的目的,如在TcpMgr中也保存DataMgr的实例(在交叉引用时需要进行类的前置声明)。这里介绍使用回调函数的方式。

思路是:在 TcpMgr 中定义一个 std::function 对象,然后提供一个接口供外部调用来设置该对象,再使用该对象处理数据。具体做法如下:

  • 在 TcpMgr 中定义 std::function 对象,如:std::function m_handleDataCb;
  • 提供一个接口供外部调用来设置该对象,如:void SetDataHandleCb(std::function cb) { m_handleDataCb = cb; }
  • 在 DataMgr 中使用 SetDataHandleCb 设置回调函数
  • 使用该对象处理数据:m_handleDataCb(msg);

示例代码如下:

// DataMgr.h文件

#include "TcpMgr.h"

class DataMgr 
{
public:
	void Init()
	{
		m_pTcpMgr = std::make_shared<TcpMgr>();
		m_pTcpMgr->SetDataHandleCb(std::bind(&DataMgr::HandleData, this, _1)); // 在初始化中为 TcpMgr 设置回调函数
	}
	void HandleData(const string &data)
	{
		// ...
	}
	
private:
	boost::shared_ptr<TcpMgr> m_pTcpMgr;
};

// TcpMgr.h文件
class TcpMgr
{
public:
	void SetDataHandleCb(std::function<void(const string &)> cb)
	{
		m_handleDataCb = cb; 
	}
	void OnRecvedData(const string &data)
	{
		// 调用DataMgr::HandleData
		m_handleDataCb(data);
	}
private:
	std::function<void(const string &)> m_handleDataCb;
}

如上,可以避免交叉引用带来的一些问题,较好地实现类间的数据处理关系。

小结


回调函数在实际项目中使用普遍,c11中提供了Lambda表达式等便捷的方式,在实际编程中得当地使用可以显著提高程序可读性和可维护性。

参考资料

std::function
c++中的Lambda表达式

你可能感兴趣的:(cpp)