简介
类型安全机制的实现原来采用的是C风格的回调(callback)函数,而.NET Framework引入了委托和事件来替代原来的方式;它们被广泛地使用。
我们在这里尝试使用标准C++来实现与之类似的功能,这样我们不但可以对这些概念有一个更好的熟悉,而且同时还能够体验C++的一些有趣的技术。
C#中的委托与事件要害字
首先我们来看一个简单的C#程序(下面的代码略有删节)。执行程序的输出结果如下显示:
SimpleDelegateFunction called from Ob1,
string=Event fired!
Event fired!(Ob1): 3:49:46 PM on
Friday, May 10, 2002
Event fired!(Ob1): 1056318417
SimpleDelegateFunction called from Ob2,
string=Event fired!
Event fired!(Ob2): 3:49:46 PM on
Friday, May 10, 2002
Event fired!(Ob2): 1056318417
所有这些都源于这样一行代码:dae.FirePRintString("Event fired!");
在利用C++来实现这些功能时,我模拟了C#的语法并完全按照功能的要求进行开发。
namespace DelegatesAndEvents
{
class DelegatesAndEvents
{
public delegate void PrintString(string s);
public event PrintString MyPrintString;
public void FirePrintString(string s)
{
if (MyPrintString != null)MyPrintString(s);
}
}
class TestDelegatesAndEvents
{
[STAThread]
static void Main(string[] args)
{
DelegatesAndEvents dae =new DelegatesAndEvents();
MyDelegates d = new MyDelegates();
d.Name = "Ob1";
dae.MyPrintString +=new DelegatesAndEvents.PrintString(d.SimpleDelegateFunction);
// ... more code similar to the
// above few lines ...
dae.FirePrintString("Event fired!");
}
}
class MyDelegates
{
// ... "Name" property omitted...
public void SimpleDelegateFunction(string s)
{
Console.WriteLine("SimpleDelegateFunction called from {0}, string={1}", m_name, s);
}
// ... more methods ...
}
}
更多内容请看.NET开发手册 .NET实用开发 .NET移动与嵌入式技术专题,或C++中的类型安全函数指针
对于“老式方法”的批判之一便是它们不是类型安全的[1]。下面的代码证实了这个观点:
typedef size_t (*FUNC)(const char*);
void printSize(const char* str) {
FUNC f = strlen;
(void) printf("%s is %ld chars\n", str, f(str));
}
void crashAndBurn(const char* str) {
FUNC f = reinterpret_cast<FUNC>(strcat);
f(str);
}
代码在[2]中可以找到。当然,在你使用reinterpret_cast的时候,你可能会碰到麻烦。假如你将强制转换(cast)去掉,C++编译器将报错,而相对来说更为安全的static_cast也不能够完成转换。这个例子也有点像比较苹果和橙子,因为在C#中万事万物皆对象,而reinterpret_cast就相当于一种解决方式。下面的这个C++程序示例将会采取使用成员函数指针的方法来避免使用reinterpret_cast:
strUCt Object { };
struct Str : public Object {
size_t Len(const char* str) {
return strlen(str);
}
char* Cat(char* s1, const char* s2) {
return strcat(s1, s2);
}
};
typedef size_t (Object::*FUNC)(const char*);
void printSize(const char* s) {
Str str;
FUNC f = static_cast<FUNC>(&Str::Len);
(void) printf("%s is %ld chars\n", s, (str.*f)(s));
}
void crashAndBurn(const char* s) {
Str str;
FUNC f = static_cast<FUNC>(&Str::Cat);
(str.*f)(s);
}
static_cast运算符将转化Str::Len函数指针,因为Str是由Object派生来的,但是Str::Cat是类型安全的,它不能被转换,因为函数签名是不匹配的。
成员函数指针的工作机制与常规的函数指针是非常相似的;唯一不同(除了更为复杂的语法外)的是你需要一个用来调用成员函数的类的实例。当然,我们也可以使用->*运算符来用指向类实例的指针完成对成员函数的调用。
Str* pStr = new Str();
FUNC f = static_cast<FUNC>(&Str::Len);
(void) printf("%s is %ld chars\n", s, (str->*f)(s));
delete pStr;
只要所有的类是从基类Object派生来的(C#中就是这样),你就可以使用C++来创建类型安全的成员函数指针。
更多内容请看.NET开发手册 .NET实用开发 .NET移动与嵌入式技术专题,或创建一个委托类
拥有类型安全成员函数指针是我们效仿.NET功能的第一部。尽管如此,单独的成员函数指针是毫无用处的 — 你总是需要一个类的实例;委托对象同时保持在两边,使得调用成员函数非常方便。我们接着上面的例子续写下面的代码:
struct StrLen_Delegate
{
typedef size_t (Str::*MF_T)(const char*);
MF_T m_method;
Object& m_pTarget;
StrLen_Delegate(Object& o, const MF_T& mf) :
m_pTarget(&o), m_method(mf) {}
MF_T Method() const {
return m_method;
}
Object& Target() const {
return *m_pTarget;
}
size_t Invoke(const char* s) {
(m_pTarget.*m_method)(s);
}
};
void printSize2(const char* s) {
Str str;
StrLen_Delegate d(str, &Str::Len);
(void) printf("%s is %ld chars\n", s,
d.Invoke(s));
}
有了委托类,调用成员函数变得更为简单。使用运算符代替Invoke来给这个类创建一个仿函数将使调用降为仅有d(s);为了清楚以及和.NET规定匹配,我使用Invoke。需要注重的是,类的实例是一个对象(Object)而不是Str。只要签名匹配,从Object派生来的任何一个类的成员函数指针将答应被用于创建委托。
这个类在这个例子中使用能够工作得非常好,但是它不是非常灵活;我们必须为每一个可能的成员函数签名写一个新的委托类。.NET使用由公用语言运行时(Common Language Runtime)维护的rich type信息来解决这个问题。但这在C++中不是一个非常可行的办法,但是可以采用模板来完成类似的功能。我们不用将Invoke函数的参数设为const char* s,而是将类型指定为模板参数:
template <typename ARG1>
struct StrLen_Delegate
{
typedef size_t (Str::*MF_T)(ARG1);
// ... as above ...
size_t Invoke(ARG1 v1) {
(m_pTarget.*m_method)(v1);
}
};
这样效果就好很多了,但是Invoke函数将只作用于单参数的成员函数。并且,委托也仅仅关心类的实例以及成员函数指针;它不是真正关心成员函数指针的细节。最后,我们很方便地就能够为成员函数指针产生一个typedef作为模版参数使用。由于一切都是由Object类派生出来的,这些细节也可以被移动到Object当中:
struct Object
{
template <typename ARG1>
struct void1_T {
typedef void (Object::*mf_t)(ARG1);
};
template <typename ARG1, typename ARG2>
void Invoke(void1_T<ARG1>::mf_t mf, ARG1 v1, ARG2) const {
(this->*mf)(v1);
}
};
template <typename CLASS>
class ObjectT : public Object {};
typedef ObjectT<void> VoidType;
这个Object基类包含了一个typedef对应每一个成员函数签名;我使用了void返回类型来简化了很多需要做的工作。Typedef可以参照如下方式使用:
typedef Object::void1_T<std::string>::mf_t StringMF_t;
我们使用了std::string类型的参数和void返回类型就能够非常轻易地为成员函数指针创建typedef。
程序根据附加的参数对于Invoke是跟踪计数的。这是非常必要的,因为对于所有的Invoke方法必须有同样数目的参数;重载决策基于第一个参数—成员函数指针的类型,来完成。需要注重的是大部分的.NET Framework将在委托中使用EventArgs对象来避免上述的复杂情况。你可以通过从EventArgs派生来添加额外的参数而不需要给委托添加签名。
最后,ObjectT模版提供了一个简单的方法用来产生唯一类型,每一个类型最终是从Object派生来的。这就确保了类型安全。
基于上面所有的内容,委托类现在就应当是如下所示的样子:
template <typename MF_T>
class DelegateT_ : public ObjectT<MF_T>
{
MF_T m_method;
Object* m_pTarget;
protected:
DelegateT_() : m_pTarget(NULL), m_method(NULL) {}
DelegateT_(Object& o, const MF_T& mf) :
m_pTarget(&o), m_method(mf) {}
public:
MF_T Method() const {
return m_method;
}
Object& Target() const {
return *m_pTarget;
}
};
模板参数现在就是一个typedef成员函数指针(生成方法如上所示),而Invoke方法继续于Object基类。
更多内容请看.NET开发手册 .NET实用开发 .NET移动与嵌入式技术专题,或 维护委托集在C#中,Delegate和Event要害字成对出现用来创建一列委托,就像上面的第一个例子:
new DelegatesAndEvents.PrintString(d.SimpleDelegateFunction);
创建一个新的类似于我的C++实现的委托对象:
StrLen_Delegate d(str, &Str::Len);
MyPrintString对象是一个拥有重载运算符+=的事件,这是用来添加委托的。在C++中我们也可以模拟这个功能来完成类似的工作。C#中的Delegate要害字创建了一个MultiCastDelegate对象(详见[3])。你会注重到我将上面的委托类命名为DelegateT_(尾随的下划线说明这个名字是保留的)。严格地说,名字_DelegateT是为这个程序实现而保留的(__DelegateT也是一样的)因为下划线后跟随着一个大写字母。_delegateT也可以(仅有一个被小写字母尾随其后的下划线),但是我偏向于避免所有的由于前下划线所可能导致的潜在错误(阅读我写的代码的人很可能抓不到我的所有规则)也不愿意采用后划线代替它。保留DelegateT_是因为完成效仿.NET功能的委托类是从多播委托(MultiCastDelegate)类派生来的。
Delegate对象可以很轻易地被存储在标准C++容器中。我将使用list,因为它与.NET的工作机制是最接近的。依据你个人的需要,也可以使用vector或者deque。使用集(set)来提供不论委托被附加入几次,仅仅调用一次的有趣的特性。MultiCastDelegate的第一部分如下所示:
template <typename MF_T, typename ARG1 = VoidType,
typename ARG2 = VoidType>
class MulticastDelegateT : public DelegateT_<MF_T>
{
typedef DelegateT_<MF_T> Delegate;
typedef std::list<Delegate> Delegates_t;
protected:
MulticastDelegateT() {}
public:
MulticastDelegateT(Object& o, const MF_T& mf) :
Delegate(o, mf) {}
MulticastDelegateT& Operator+=(const Delegate& d) {
m_delegates.push_back(d);
return *this;
}
private:
Delegates_t m_delegates;
};
这里使用了list和几个typedef来存储委托集。它需要从DelegateT_派生而来,因为下面我将从MultiCastDelegateT派生出DelegateT作为真正的委托类。
而后激发所有被存储的委托上的一个C#循环中的事件并调用每一个。因为我使用的是标准容器,使迭代器将很方便:
void operator()(ARG1 v1 = VoidType(),
ARG2 v2 = VoidType()) const {
for (Delegates_t::const_iterator it = m_delegates.begin();
it != m_delegates.end(); ++it)
(it->Target()).Invoke(it->Method(), v1, v2);
}
即使你很适应标准C++容器,这可能也是你不熟悉的一行代码:只在一个模版类中就可以使用迭代器调用成员函数!对迭代器取反引用,我们可以清楚地看到发生了什么:
const Delegate& d = *it;
d.Invoke(d.Method(), v1, v2);
假如你对迭代器还不是很适应,你可以指出一个就像数组一样的deque:
for (int i=0; i<m_delegates.size(); i++)
Delegate d = m_delegates[i];
在这里,你可以为DelegateT_ 类添加下面的模板成员函数:
template <typename ARG1, typename ARG2>
void Invoke_(ARG1 v1 = ARG1(), ARG2 v2 = ARG2()) const {
this->Invoke(m_method, v1, v2);
}
这样就避免了MultiCastDelegateT::Invoke方法一定要将成员函数指针传递给Object::Invoke:
d.Invoke_(v1, v2);
尽管如此,这将需要每一个参数都有一个默认构造函数,但事实却不见得如此。并且,由于MultiCastDelegateT是真正的委托基类,看上去并没有太大的必要调用Object::Invoke 路径—即使由于这个原因代码显得更为复杂。(这也会在Visual C++.NET中导致可怕的“内部编译器错误”)。
实际的委托类现在仅仅是MultiCastDelegateT的一个简单的包装:
template <typename MF_T, typename ARG1 = VoidType,
typename ARG2 = VoidType>
struct DelegateT :
public MulticastDelegateT<MF_T, ARG1, ARG2>
{
DelegateT(Object& o, const MF_T& mf) :
MulticastDelegateT<MF_T, ARG1, ARG2>(o, mf) {}
DelegateT() {}
typedef DelegateT<MF_T, ARG1, ARG2> Event;
};
它的主要功能是提供事件typedef。
将他们集成起来
现在你可以用C++编写实现C#例子当中的DelegatesAndEvents类了:
class DelegatesAndEvents
{
// C#: public delegate void PrintString(string s);
typedef DelegateT<Object::void1_T<std::string>::mf_t,
std::string> PrintString_;
public:
template <typename OBJECT>
static PrintString_ PrintString(OBJECT& o,
void (OBJECT::*mf)(std::string)) {
return PrintString_(o,
static_cast<Object::void1_T<std::string>::mf_t>(mf));
}
// C#: public event PrintString MyPrintString;
PrintString_::Event MyPrintString;
void FirePrintString(std::string s) {
MyPrintString(s);
}
};
这样的语法看上去着实令人恐怖,假如你愿意,可以用一些灵巧的宏来简化它。但最近宏的名声不太好,并且我们进行的这个主题要害是要了解细节。无论怎样,你都应当感谢C#编译器为你做的工作。
第一行代码创建一个成员函数指针私有的typedef,名称为PrintString_。参数类型std::string需要列两次,这太糟了,但是这正是由于Visual C++不支持局部模版特化造成的。static方法为创建你自己的类型的委托提供了一个方便的方法,答应你这样来写你的代码:
DelegatesAndEvents::PrintString_
myDelegate = DelegatesAndEvents::PrintString(d,&MyDelegates::SimpleDelegateFunction);
这与上面的C#代码是类似的。
而后,我们使用来自DelegateT_的Event typedef创建事件。请注重这一系列的typedef是如何答应C++代码至少是有C#代码一些类似之处的。最后,有一个方法触发事件,这与C#尤其相同。(由于你采用的是标准容器,所以不必担心NULL列表。)
使用委托和事件的客户端的代码就很明了了,而且也很类似于C#代码(同样这些代码也是略有缩减的):
struct MyDelegates : public ObjectT<MyDelegates>
{
// ... Name omitted...
void SimpleDelegateFunction(std::string s)
{
printf("SimpleDelegateFunction called from %s,
string=%s\n", m_name.c_str(), s.c_str());
}
// ... more methods ...
};
void CppStyle()
{
DelegatesAndEvents dae;
MyDelegates d;
d.Name() = "Obj1";
dae.MyPrintString += DelegatesAndEvents::PrintString
(d, &MyDelegates::SimpleDelegateFunction);
// ... more code similar to the above few lines ...
dae.FirePrintString("Event fired!");
}
请注重MultiCastDelegateT::operator+=是如何被调用来为委托列表添加每一个由静态方法DelegatesAndEvents::PrintString返回的委托的。
更多内容请看.NET开发手册 .NET实用开发 .NET移动与嵌入式技术专题,或
托管C++
由于委托和事件是.NET框架的一部分,所有的.NET支持的语言都可以使用它们。我所描述的基于模版的实现是专门针对C++的。Microsoft采用了不同的方法在C++中将这个功能公开—对于标准C++的扩展称为托管C++。也许你并不感到太吃惊,在托管C++中编写这个例子与最初的代码是那么相似:
public __gc struct DelegatesAndEvents {
__event void MyPrintString(String* s);
void FirePrintString(String* s) {
MyPrintString(s);
}
};
__gc struct MyDelegates
{
String* Name;
void SimpleDelegateFunction(String* s) {
Console::WriteLine
("SimpleDelegateFunction called from {0} string={1}",Name, s);
}
};
void ManagedCpp()
{
DelegatesAndEvents* dae = new DelegatesAndEvents();
MyDelegates* d = new MyDelegates();
d->Name = "Obj1";
__hook(&DelegatesAndEvents::MyPrintString, dae,
&MyDelegates::SimpleDelegateFunction, d);
dae->FirePrintString(S"Event fired!");
}
要害字__gc标志着这个类是被垃圾回收机制控制的(托管的);我们不需要调用delete函数。仅仅一个__event要害字就完成了我们上面代码的大部分功能。需要注重的是托管C++使用__hook要害字来替代上面讨论的操作符+=。你会发觉使用-Fx标记[4]调用(托管)C++编译器编译上述代码和检查产生的结果文件.mrg非常有趣。在编译器级加入新功能而不是编写模板显然要轻易得多了。
结论
通过使用极为高级的C++技巧,我已经向大家展示了用C++为简单的样例代码实现委托与事件是可行的。这个实现主要考虑基于.NET框架。更为一流和纯粹的C++解决方案可以使用C++标准库中的适配器和联编程序。
参考文献
[1] Jeffrey Richter. “An Introduction to Delegates,” MSDN Magazine, April 2001.
< http://msdn.microsoft.com/msdnmag/issues/01/04/net/default.aspx >.
[2] Richard Grimes. “.NET Delegates: Making Asynchronous Method Calls in the .NET Environment,” MSDN Magazine, August 2001.
<http://msdn.microsoft.com/msdnmag/issues/01/08/Async/default.aspx>.
[3] Jeffrey Richter. “Delegates, Part 2,” MSDN Magazine, June 2001.
< http://msdn.microsoft.com/msdnmag/issues/01/06/net/default.aspx>
[4] Bobby Schmidt. “The Red Pill,” April 23, 2002.
<http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dndeepc/Html/deep04232002.asp >
译者注:
译注1:Type-safe:按照2003年微软官方提供的术语表翻译为“类型安全”。
译注2:overload resolution: 按照2003年微软官方提供的术语表翻译为“重载决策”。
译注3:原文中所列参考文献的地址已经失效,译文中提供的是在本文翻译截稿时所示参考的最新有效链接,为尊重原著者特此说明。
译注4:destructor一词按照简体中文常用译法译为“反引用”。
译注5:关于文中采用的reinterpret_cast。事实上,reinterpret_cast在这里是通不过的。因为我们不可能对成员函数指针进行所谓的类型转换。这个例子实际上是在比较对象,转换的也是对象,而不是对象的成员。而这个示例却将reinterpret_cast作为解决的方式,即直接比较的是对象的成员,而不考虑对象。也就是说,试图转换对象的成员。而失去类型转换的真正意图。为什么作者在这里用了reinterpret_cast,意为“重新意义上的强制转换“。这种转换并不是基于类型或者是对象的,更谈不上类型安全了。委托的本质上讲是函数指针,不过,它需要首先进行类型检查。我们说委托对象的存在,只是为了类型检查,真正有意义的还是其方法。所以reinterpret_cast相当于一种解决方式。
更多内容请看.NET开发手册
.NET实用开发 .NET移动与嵌入式技术专题,或