2004年08月27日

        在2005年即将推出的Visual C++2005版本中,实现了绝大部分的C++/CLI规范。这也意味着Version 1中的语法定义的过时,很多程序员刚刚适应了__gc等等语法定义形式,又要被迫作一次转换。诚然最新的语法定义是向着好的方向转化,也让人们觉得舒服很多,但是对于企业来讲又要对员工进行一次筛选和再培训。但是这将会不同程度的耽误企业的项目进度。

        在未来的Beta2或者正式版中,微软将在集成开发环境中加入一个源代码转换(翻译)工具,称为“mscfront”他将可以将你现有的VC++.NET代码自动转换成C++/CLI代码。这解决了企业的后顾之忧。然而还是认为应当尽快地熟悉C++/CLI规范。

想要真正掌握委托与事件最好的方法便是你自己来实现它们使用以前的纯C++

简介

类型安全机制的实现原来采用的是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 …

             }

}

 

 

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(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(&Str::Len);

(void) printf(“%s is %ld chars\n”, s, (str.*f)(s));

}

void crashAndBurn(const char* s) {

Str str;

FUNC f = static_cast(&Str::Cat);

(str.*f)(s);

}

 

static_cast运算符将转化Str::Len函数指针,因为Str是由Object派生来的,但是Str::Cat是类型安全的,它不能被转换,因为函数签名是不匹配的。

成员函数指针的工作机制与常规的函数指针是非常相似的;唯一不同(除了更为复杂的语法外)的是你需要一个用来调用成员函数的类的实例。当然,我们也可以使用->*运算符来用指向类实例的指针完成对成员函数的调用。

Str* pStr = new Str();

FUNC f = static_cast(&Str::Len);

(void) printf(“%s is %ld chars\n”, s, (str->*f)(s));

delete pStr;

 

 

只要所有的类是从基类Object派生来的(C#中就是这样),你就可以使用C++来创建类型安全的成员函数指针。

创建一个委托类

拥有类型安全成员函数指针是我们效仿.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::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 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:

维护委托集

C#中,DelegateEvent关键字成对出现用来创建一列委托,就像上面的第一个例子:

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_

{

typedef DelegateT_ Delegate;

typedef std::list 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

{

DelegateT(Object& o, const MF_T& mf) :

MulticastDelegateT(o, mf) {}

DelegateT() {}

typedef DelegateT Event;

};

?

它的主要功能是提供事件typedef

将他们集成起来

现在你可以用C++编写实现C#例子当中的DelegatesAndEvents类了:

class DelegatesAndEvents

{

// C#: public delegate void PrintString(string s);

typedef DelegateTstd::string>::mf_t,

std::string> PrintString_;

public:

template <typename OBJECT>

static PrintString_ PrintString(OBJECT& o,

void (OBJECT::*mf)(std::string)) {

return PrintString_(o,

static_caststd::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返回的委托的。

托管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.

.

[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.

作者简介

J. Daniel Smith 是密歇根州Novi的一位持有Autodesk认证的软件工程师。他从加尔文学院取得了理学学士,并在密歇根州立大学取得了计算机科学的理学硕士学位。你可以通过 cuj@jdanielsmith.org 与他取得联系。

译者注:

译注1Type-safe:按照2003年微软官方提供的术语表翻译为类型安全

译注2overload resolution: 按照2003年微软官方提供的术语表翻译为重载决策

译注3原文中所列参考文献的地址已经失效,译文中提供的是在本文翻译截稿时所示参考的最新有效链接,为尊重原著者特此说明。

译注4destructor一词按照简体中文常用译法译为反引用

译注5关于文中采用的reinterpret_cast。事实上,reinterpret_cast在这里是通不过的。因为我们不可能对成员函数指针进行所谓的类型转换。这个例子实际上是在比较对象,转换的也是对象,而不是对象的成员。而这个示例却将reinterpret_cast作为解决的方式,即直接比较的是对象的成员,而不考虑对象。也就是说,试图转换对象的成员。而失去类型转换的真正意图。为什么作者在这里用了reinterpret_cast,意为重新意义上的强制转换。这种转换并不是基于类型或者是对象的,更谈不上类型安全了。委托的本质上讲是函数指针,不过,它需要首先进行类型检查。我们说委托对象的存在,只是为了类型检查,真正有意义的还是其方法。所以reinterpret_cast相当于一种解决方式。

MF_T Method() const {

return m_method;

}

Object& Target() const {

return *m_pTarget;

}

};

 

模板参数现在就是一个typedef成员函数指针(生成方法如上所示),而Invoke方法继承于Object基类。