更新: The Code Project上发布了具有完整源代码和更详细讨论的文章。
好吧,方法指针的问题在于它们的大小不尽相同。因此,与其直接存储指向方法的指针,不如将它们“标准化”,以使它们的大小恒定。这就是Don Clugston在其“代码项目”文章中试图实现的目标。他使用最流行的编译器的深入知识来这样做。我断言可以在“普通” C ++中完成此操作而无需掌握此类知识。
考虑以下代码:
void DoSomething(int)
{
}
void InvokeCallback(void (*callback)(int))
{
callback(42);
}
int main()
{
InvokeCallback(&DoSomething);
return 0;
}
这是使用普通的旧函数指针实现回调的一种方法。但是,这不适用于对象中的方法。让我们解决这个问题:
class Foo
{
public:
void DoSomething(int) {}
static void DoSomethingWrapper(void* obj, int param)
{
static_cast<Foo*>(obj)->DoSomething(param);
}
};
void InvokeCallback(void* instance, void (*callback)(void*, int))
{
callback(instance, 42);
}
int main()
{
Foo f;
InvokeCallback(static_cast<void*>(&f), &Foo::DoSomethingWrapper);
return 0;
}
现在,我们有了一个回调系统,该回调系统可以同时用于免费函数和成员函数。然而,这是笨拙且容易出错的。但是,有一种模式-使用包装函数将静态函数调用“转发”到适当实例上的方法调用。我们可以使用模板来解决这个问题-让我们尝试概括包装函数:
template<typename R, class T, typename A1, R (T::*Func)(A1)>
R Wrapper(void* o, A1 a1)
{
return (static_cast<T*>(o)->*Func)(a1);
}
class Foo
{
public:
void DoSomething(int) {}
};
void InvokeCallback(void* instance, void (*callback)(void*, int))
{
callback(instance, 42);
}
int main()
{
Foo f;
InvokeCallback(static_cast<void*>(&f),
&Wrapper<void, Foo, int, &Foo::DoSomething> );
return 0;
}
这仍然非常笨拙,但是至少现在我们不必每次都写出包装函数了(至少对于1参数情况而言)。我们可以概括的另一件事是,我们一直在传递一个指向的指针void*
。与其将它传递为两个不同的值,不如将它们放在一起?在我们这样做的同时,为什么不将其概括呢?嘿,让我们扔一个,operator()()
以便我们可以像函数一样调用它!
template<typename R, typename A1>
class Callback
{
public:
typedef R (*FuncType)(void*, A1);
Callback(void* o, FuncType f) : obj(o), func(f) {}
R operator()(A1 a1) const
{
return (*func)(obj, a1);
}
private:
void* obj;
FuncType func;
};
template<typename R, class T, typename A1, R (T::*Func)(A1)>
R Wrapper(void* o, A1 a1)
{
return (static_cast<T*>(o)->*Func)(a1);
}
class Foo
{
public:
void DoSomething(int) {}
};
void InvokeCallback(Callback<void, int> callback)
{
callback(42);
}
int main()
{
Foo f;
Callback<void, int> cb(static_cast<void*>(&f),
&Wrapper<void, Foo, int, &Foo::DoSomething>);
InvokeCallback(cb);
return 0;
}
我们正在进步!但是现在我们的问题是语法绝对可怕。语法显得多余;编译器无法从方法本身的指针中找出类型吗?不幸的是,没有,但是我们可以提供帮助。请记住,编译器可以通过函数调用中的模板参数推导来推断类型。那为什么不从那开始呢?
template<typename R, class T, typename A1>
void DeduceMemCallback(R (T::*)(A1)) {}
在函数内部,我们知道什么R
,T
和A1
是。那么,如果我们可以构造一个可以“保留”这些类型并从函数中返回它们的结构,该怎么办?
template<typename R, class T, typename A1>
struct DeduceMemCallbackTag
{
};
template<typename R, class T, typename A1>
DeduceMemCallbackTag2<R, T, A1> DeduceMemCallback(R (T::*)(A1))
{
return DeduceMemCallbackTag<R, T, A1>();
}
而且既然DeduceMemCallbackTag
知道类型,为什么不将包装函数作为静态函数放在其中呢?而且由于包装函数在其中,为什么不在其中放入用于构造Callback
对象的代码呢?
template<typename R, typename A1>
class Callback
{
public:
typedef R (*FuncType)(void*, A1);
Callback(void* o, FuncType f) : obj(o), func(f) {}
R operator()(A1 a1) const
{
return (*func)(obj, a1);
}
private:
void* obj;
FuncType func;
};
template<typename R, class T, typename A1>
struct DeduceMemCallbackTag
{
template<R (T::*Func)(A1)>
static R Wrapper(void* o, A1 a1)
{
return (static_cast<T*>(o)->*Func)(a1);
}
template<R (T::*Func)(A1)>
inline static Callback<R, A1> Bind(T* o)
{
return Callback<R, A1>(o, &DeduceMemCallbackTag::Wrapper<Func>);
}
};
template<typename R, class T, typename A1>
DeduceMemCallbackTag<R, T, A1> DeduceMemCallback(R (T::*)(A1))
{
return DeduceMemCallbackTag<R, T, A1>();
}
C ++标准允许我们在实例(!)上调用静态函数:
class Foo
{
public:
void DoSomething(int) {}
};
void InvokeCallback(Callback<void, int> callback)
{
callback(42);
}
int main()
{
Foo f;
InvokeCallback(
DeduceMemCallback(&Foo::DoSomething)
.Bind<&Foo::DoSomething>(&f)
);
return 0;
}
仍然是一个冗长的表达式,但是我们可以将其放入一个简单的宏(!)中:
template<typename R, typename A1>
class Callback
{
public:
typedef R (*FuncType)(void*, A1);
Callback(void* o, FuncType f) : obj(o), func(f) {}
R operator()(A1 a1) const
{
return (*func)(obj, a1);
}
private:
void* obj;
FuncType func;
};
template<typename R, class T, typename A1>
struct DeduceMemCallbackTag
{
template<R (T::*Func)(A1)>
static R Wrapper(void* o, A1 a1)
{
return (static_cast<T*>(o)->*Func)(a1);
}
template<R (T::*Func)(A1)>
inline static Callback<R, A1> Bind(T* o)
{
return Callback<R, A1>(o, &DeduceMemCallbackTag::Wrapper<Func>);
}
};
template<typename R, class T, typename A1>
DeduceMemCallbackTag<R, T, A1> DeduceMemCallback(R (T::*)(A1))
{
return DeduceMemCallbackTag<R, T, A1>();
}
#define BIND_MEM_CB(memFuncPtr, instancePtr) \
(DeduceMemCallback(memFuncPtr).Bind<(memFuncPtr)>(instancePtr))
class Foo
{
public:
void DoSomething(int) {}
};
void InvokeCallback(Callback<void, int> callback)
{
callback(42);
}
int main()
{
Foo f;
InvokeCallback(BIND_MEM_CB(&Foo::DoSomething, &f));
return 0;
}
我们可以Callback
通过添加安全布尔来增强对象。禁用相等运算符也是一个好主意,因为不可能比较两个Callback
对象。更好的是使用部分特殊化以允许“首选语法”。这给我们:
template<typename FuncSignature>
class Callback;
template<typename R, typename A1>
class Callback<R (A1)>
{
public:
typedef R (*FuncType)(void*, A1);
Callback() : obj(0), func(0) {}
Callback(void* o, FuncType f) : obj(o), func(f) {}
R operator()(A1 a1) const
{
return (*func)(obj, a1);
}
typedef void* Callback::*SafeBoolType;
operator SafeBoolType() const
{
return func != 0? &Callback::obj : 0;
}
bool operator!() const
{
return func == 0;
}
private:
void* obj;
FuncType func;
};
template<typename R, typename A1>
void operator==(const Callback<R (A1)>&, const Callback<R (A1)>&);
template<typename R, typename A1>
void operator!=(const Callback<R (A1)>&, const Callback<R (A1)>&);
template<typename R, class T, typename A1>
struct DeduceMemCallbackTag
{
template<R (T::*Func)(A1)>
static R Wrapper(void* o, A1 a1)
{
return (static_cast<T*>(o)->*Func)(a1);
}
template<R (T::*Func)(A1)>
inline static Callback<R (A1)> Bind(T* o)
{
return Callback<R (A1)>(o, &DeduceMemCallbackTag::Wrapper<Func>);
}
};
template<typename R, class T, typename A1>
DeduceMemCallbackTag<R, T, A1> DeduceMemCallback(R (T::*)(A1))
{
return DeduceMemCallbackTag<R, T, A1>();
}
#define BIND_MEM_CB(memFuncPtr, instancePtr) \
(DeduceMemCallback(memFuncPtr).Bind<(memFuncPtr)>(instancePtr))
用法示例:
class Foo
{
public:
float DoSomething(int n) { return n / 100.0f; }
};
float InvokeCallback(int n, Callback<float (int)> callback)
{
if(callback) { return callback(n); }
return 0.0f;
}
int main()
{
Foo f;
float result = InvokeCallback(97, BIND_MEM_CB(&Foo::DoSomething, &f));
return 0;
}
我已经在Visual C ++编译器(版本15.00.30729.01,VS 2008附带的版本)上对其进行了测试,您确实需要使用相当新的编译器才能使用该代码。通过检查反汇编,编译器可以优化包装函数和DeduceMemCallback
调用,从而简化为简单的指针分配。
在回调的两边使用都很简单,并且仅使用(我相信是)标准C ++。我上面显示的代码适用于带有1个参数的成员函数,但可以推广到更多参数。也可以通过允许支持静态功能来进一步推广。
请注意,该Callback
对象不需要分配堆-由于采用了“标准化”过程,它们的大小是恒定的。因此,由于Callback
对象具有默认构造函数,因此可以使其成为较大类的成员。它也是可分配的(编译器生成的副本分配功能已足够)。由于有了模板,它也是类型安全的。