今天来说说 boost::bind 和 operator()。这两天在折腾 asio,被回调函数传参的问题搞得头痛不已,看到官方的例子不少是拿 boost::bind 来处理的,觉得挺方便的,特意记录一下。另外,boost::bind 好像是在 c++11 中被纳入,因此也可以使用 std::bind。
什么是 operator()? operator() 指的是小括号运算符。那么哪里会用到小括号运算符呢?函数。当一个类重载了 operator() 运算符,那么它会变成一个对象函数。
那么这有什么好处呢?举个例子,在 asio 中需要注册异步操作结束时的回调函数,这个回调函数只接收两个参数,即
void handler(const asio::error_code &error, size_t bytes);
有时候我们可能希望传入其他的参数,比如传入当前Socket的引用以便启动下一次异步操作,这时函数对象就派上用场了。我们可以构造一个类,将它的operator()重载为需要的形式,然后在构造函数中传入所需变量的引用,将这些引用保存为类的成员变量即可。这样,在通过构造函数将类实例化之后,就可以像一个函数一样使用这个类了。
讲道理,这个 operator() 重载我上周看 C++ Primer 的时候刚看到过,不过万万没想到还能这么玩就是了……
boost::bind 提供了一个非常方便的模板,可以从一个函数或非静态成员函数中构造所需的函数对象,举个例子:
void CameraStreamer::tcpListener(const asio::error_code &error, size_t bytes_transferred, tcp::socket &tcpSocket, vector<uint8_t> &tcpBuffer) { // Remote close TCP connection -> log, stop ioService then return if (error == asio::error::eof) { LOG(INFO) << "Remote close TCP connection."; ioService.stop(); return; } if (error) { DLOG(ERROR) << "Some Error Occurred."; } else { DLOG(INFO) << "TCP received " << bytes_transferred << " bytes."; } tcpSocket.async_receive(asio::buffer(tcpBuffer), bind(&CameraStreamer::tcpListener, this, placeholders::_1, placeholders::_2, ref(tcpSocket), ref(tcpBuffer))); }
这个函数是 CameraStreamer 类的成员函数,当 asio 触发此回调的时候,它首先检查是否是TCP连接关闭的消息,然后检查是否有其他错误发生,随后处理收到的内容(这里仅记录了log),最后启动下一次异步数据接收。
由于在启动下一次异步接收时,函数的形参表并不匹配,因此使用了 std::bind,这里使用了非静态成员函数的绑定方法,其第一个参数为函数名,第二个参数为所属对象或所属对象的指针,剩下的参数与该函数的形参表依次匹配。std::placeholders::_1为占位符,代表 std::bind 生成对象的 operator() 的第一个参数,其余占位符也是同理。std::ref() 表示以引用的形式传入,否则会以值的形式传入。
这样,这个函数对象就可以跟 tcp::socket 所要求的异步handler形式所匹配了,顺利解决了这一问题。当然,由于使用了引用,需要小心的管理变量的生命周期。
这种处理方式有什么坏处吗?当然有,bind作为一个通用的模板,包含相当大量的重载等,因此在编译时需要花费大量时间在模板匹配上。不过相比于编写代码的方便性,这还是可以接受的。
那么,在没有这种机制的情况下,库的设计者如何处理这个问题呢?答案是 void* 指针。这种情况下一般允许用户传入一个 void* 指针,然后当事件触发时,会再把这个指针交给 handler。我们可以定义一个 struct 来存储要传入的数据,然后对 void* 指针进行强制类型转换即可。