这个问题的起源是想把成员函数封装为回调函数,众所周知一个比较好的选择是tr1里的function和bind。
function对不同类型的函数指针进行统一的封装。例如:
int foo() {...}
std::tr1::function<int ()> bar = f;
bar();
如果函数带参数或者是成员函数,使用bind可以实现一个闭包的作用,将对象本身、参数都封装进来,返回一个function对象。例如:
struct X {
int foo(int data) {
std::cout << "X::foo " << data << std::endl;
return 0;
}
};//X
X x;
std::tr1::function<int (int)> f = std::tr1::bind(&X::foo, &x, std::tr1::placeholders::_1);
f(1);
使用bind和function,可以很轻松的打平普通函数和C++成员函数在作为回调函数上使用的区别(静态成员函数等同于普通函数,因为即便对于普通函数,也可能存在namespace)。
1. 那么思考一下,bind和function是如何实现的呢?
回到闭包的定义,其实只要把函数指针、函数参数以及对象指针封装到一个结构里就可以了。这里主要面临两个问题:
- 参数类型不同
- 参数个数不同
解决方案也很简单:
- 使用模板
- 为可能的参数个数都定义一个闭包的结构体。
没错!就是可能有多少个参数,我们就定义多少个对应的闭包结构体。
2 使用
先来看下实现后的用法。
最近正好在看protobuf的源码,在google/protobuf/stubs/common.h中有一个类似的实现。
试着实现了一个简化的版本,先看下使用方法:
void foo() {
std::cout << "foo" << std::endl;
}
template<typename type>
void foo(type data) {
std::cout << "foo data=" << data << std::endl;
}
class Foo {
public:
void foo() {
std::cout << "Foo::foo" << std::endl;
}
template<typename type>
void foo1(type data) {
std::cout << "Foo::foo data=" << data << std::endl;
}
};//Foo
int main()
{
Foo f;
Closure* closure;
std::cout << "address of object in main : " << &f << std::endl;
closure = NewCallback(foo);
closure->Run();
closure = NewCallback(&f, &Foo::foo);
closure->Run();
closure = NewCallback(foo<const char [7]>, "string");
closure->Run();
closure = NewCallback(&f, &Foo::foo1<double>, 100.0);
closure->Run();
return 0;
}
NewCallback函数返回一个new之后的closure,closure不需要手动delete。
直接运行closure的Run方法就可以运行封装前的函数及对应参数。
3 实现
逐步看下是如何实现的。
3.1 首先定义一个Closure基类:
class Closure {
public:
Closure() {}
~Closure() {}
virtual void Run() = 0;
};//Closure
基类定义了一个纯虚函数Run,用于执行封装的函数,子类需要单独提供实现。
普通函数和成员函数需要定义两个不同的闭包子类
3.2 先看下无参数情况下的普通函数对应的闭包:
class FunctionClosure0 : public Closure {
public:
typedef void (*FunctionType)();
FunctionClosure0(FunctionType f) :
_f(f) {
}
~FunctionClosure0() {
}
virtual void Run() {
_f();
delete this;
}
private:
FunctionType _f;
};//FunctionClosure0
其中,FunctionType
用于存储函数指针,注意对象本身通过NewCallback
函数new而来,因此在Run
方法里调用delete this
清理自身,防止忘了delete而导致内存泄露。
在protobuf里还提供了了NewPermanentCallback
,使用该方法产生的Closure对象在调用Run方法时,不会调用delete this
,实现上就是为Closure类增加了一个bool用于记录是否需要调用delete方法。
该Closure对应的NewCallback为:
Closure* NewCallback(void (*function)()) {
return new FunctionClosure0(function);
}
所有的NewCallback都返回指向Closure对象的指针,因此传入函数指针类型不同,但返回类型是相同的。
3.3 接着看下无参数成员函数对应的闭包:
template <typename Class>
class MethodClosure0 : public Closure {
public:
typedef void (Class::*MethodType)();
MethodClosure0(Class* object, MethodType m) :
_object(object),
_m(m) {
}
~MethodClosure0() {
}
virtual void Run() {
std::cout << "address of object in Run : " << _object << std::endl;
(_object->*_m)();
delete this;
}
private:
Class* _object;
MethodType _m;
};//MethodClosure0
可以看到跟普通函数对应的闭包类差别不大,主要有:
- 需要传入class模板
- 函数定义不同,普通函数为
typedef void (*FunctionType)()
,成员函数为typedef void (Class::*MethodType)()
- 构造函数需要传入class对应的对象指针,并保存为成员变量
- 调用时使用
(_object->*_m)()
的方式
对应的NewCallback
:
template <typename Class>
Closure* NewCallback(Class* object, void (Class::*method)()) {
return new MethodClosure0<Class>(object, method);
}
类似的,通过模板可以实现不同参数个数的闭包对象,每种情况对应需要实现两个闭包对象和两个NewCallback函数。
3.4 例如对于单参数:
template <typename Arg1>
class FunctionClosure1 : public Closure {
public:
typedef void (*FunctionType)(Arg1);
FunctionClosure1(FunctionType f, Arg1 arg1) :
_f(f),
_arg1(arg1) {
}
~FunctionClosure1() {
}
virtual void Run() {
_f(_arg1);
delete this;
}
private:
FunctionType _f;
Arg1 _arg1;
};//FunctionClosure1
template <typename Class, typename Arg1>
class MethodClosure1 : public Closure {
public:
typedef void (Class::*MethodType)(Arg1);
MethodClosure1(Class* object, MethodType m, Arg1 arg1) :
_object(object),
_m(m),
_arg1(arg1) {
}
~MethodClosure1() {
}
virtual void Run() {
std::cout << "address of object in Run : " << _object << std::endl;
(_object->*_m)(_arg1);
delete this;
}
private:
Class* _object;
MethodType _m;
Arg1 _arg1;
};//MethodClosure1
template <typename Arg1>
Closure* NewCallback(void(*function)(Arg1), Arg1 arg1) {
return new FunctionClosure1<Arg1>(function, arg1);
}
template <typename Class, typename Arg1>
Closure* NewCallback(Class* object, void (Class::*method)(Arg1), Arg1 arg1) {
return new MethodClosure1<Class, Arg1>(object, method, arg1);
}
区别就是增加了参数模板,以及在Closure子类里保存参数。
实际使用中可以选择生成多少个参数对应的类和NewCallback,代码的生成是有规律可循的,有兴趣的朋友可以写个脚本实现下。
protobuf或者其他常见的关于closure的实现里要复杂一些,例如判断对象是否赋值过类似的需求。
完整的代码示例放在了这里
4. 继续思考
完整的闭包考虑的问题比这篇文章里提到的要多得多,比如传入的对象指针如何才能保证在异步回调时没有被析构,如果已经被析构怎么样保证不出问题,这些就需要shared_ptr/weak_ptr
登场了。
又或者比如这么执行boost::function<void ()> function; function()
会不会有问题?是否有判断空值的接口,自己来实现的话,这个接口是否有必要提供?
在chrome源码bind.h里,bind的参数里会额外传入一个参数,取值为:base::Unretained(closure不拥有所有权,用于AddRef接口), base::Owned(closure拥有所有权)等,另外通过使得Foo
继承自RefCountedThreadSafe<Foo>
或者内部维护一个WeakPtrFactory<Foo>
对象等,实现了类似于弱回调的作用。同时chrome默认bind参数为const T&的形式,防止了参数的拷贝。也是值得思考的。有兴趣的同学可以看下代码和文档