.NET 风格的委托(Delegate)在非托管 C++ 中的实现






3.72/5 (13投票s)
2002 年 9 月 21 日
4分钟阅读

133472

470
本文介绍了一种设计模式,如何在普通/非托管 C++ 中实现 .NET 中所见的通用委托。
引言
C#、VB.NET 和 MC++ 的用户有一个很方便的功能:委托。C++ 语言不支持这个构造。但幸运的是,可以使用模板和从 boost 库借用的一些巧妙技巧来实现基本的委托。
我假设您有扎实的 C++ 背景。在本文中,我将使用成员函数指针和模板来解决委托问题。在继续阅读之前,您可能想先回顾一下这些主题。
什么是委托?
对于那些尚未接触过 .NET 语言的读者,这里有一个简短的解释。
简单来说,委托是允许在对象上调用方法的对象。有什么大不了的?这很重要,因为这些对象伪装成与特定对象没有任何耦合关系的自由函数。顾名思义,它将方法调用委托给目标对象。
C++ 对委托的第一次尝试
由于可以获取成员函数的地址,并在定义该成员函数的类的任何对象上应用该成员函数,因此理应可以创建一个委托构造。一种方法是存储对象的地址以及其成员函数之一。存储可以是一个重载了 operator()
的对象。operator()
的类型签名(返回类型和参数类型)应与我们要用于委托的成员函数的类型签名匹配。一个非常非动态的版本可以是
struct delegate {
type* obj;
// The object which we delegate the call to
int (type::* method)(int);
// Method belonging to type, taking an int
// and returning an int
delegate(type* obj_,
int (type::* method)(int)) : obj(obj_),
method(method_) { }
int operator()(int x) {
// See how this operator() matches
// the method type signature
return (obj->*method)(x);
// Make the call
}
};
上述解决方案在任何方面都不是动态的。它只能处理类型为 type
的对象、接受 int
并返回 int
的方法。这迫使我们为希望委托的每种对象类型/方法组合编写新的委托类型,或者使用对象多态性,其中所有类都从 type
派生——并且只能委托在 type
中定义的、与 int/int
类型签名匹配的虚方法!显然,这不是一个好的解决方案。
C++ 对委托的第二次尝试
显然,我们需要参数化对象类型、参数类型和返回类型。在 C++ 中做到这一点的唯一方法是使用模板。第二次尝试可能看起来像这样
template <typename Class, typename T1, typename Result>
struct delegate {
typedef Result (Class::* MethodType)(T1);
Class* obj;
MethodType method;
delegate(Class* obj_,
MethodType method_) : obj(obj_), method(method_) { }
Result operator()(T1 v1) {
return (obj->*method)(v1);
}
};
好多了!现在我们可以委托任何对象和该对象中的任何单参数方法。这比之前的实现有了明显的改进。
不幸的是,无法编写能够处理任意数量参数的委托。要解决具有两个参数的方法的问题,必须编写一个新的处理两个参数的委托。要解决具有三个参数的方法的问题,必须编写一个新的处理三个参数的委托——以此类推。但这也不是一个大问题。如果您需要覆盖所有方法,很可能不需要超过十个这样的委托模板。您有多少方法有超过十个参数?如果有,您确定它们应该有超过十个参数吗?另外,您只需要编写这十个委托一次——这是模板的强大之处。
然而,除了参数问题之外,还有一个小问题仍然存在。当实例化此委托模板时,生成的委托类型将只能处理您作为模板参数提供的类的委托。delegate<A, int, int>
类型与 delegate<B, int, int>
不同。它们相似之处在于它们都委托接受 int
并返回 int
的方法调用。它们不同之处在于它们不委托同一类的成员函数。.NET 委托忽略这种差异,我们也应该如此!
C++ 对委托的第三次也是最后一次尝试
为了消除这种类型差异,很明显我们需要移除类类型作为模板参数。这最好通过使用对象多态性和模板构造函数来实现。这是一项我从 boost 模板库中借用的技术。具体来说,我从该库中 any
类的实现中借用了它。
由于我不是英语母语者,我不会尝试用文字来描述最终的代码。我可以尝试,但我认为我只会让它比实际更复杂。简单来说,我使用多态性和模板构造函数来获得一个额外的间接层,这样我就可以从委托中“剥离”类信息。代码如下
// The polymorphic base
template <typename T1, typename Result>
struct delegate_base { // Ref counting added 2002-09-22
int ref_count; // delegate_base's are refcounted
delegate_base() : ref_count(0) { }
void addref() {
++ref_count;
}
void release() {
if(--ref_count < 0)
delete this;
}
virtual ~delegate_base() { } // Added 2002-09-22
virtual Result operator()(T1 v1) = 0;
};
// The actual implementation of the delegate
template <typename Class, typename T1, typename Result>
struct delegate_impl : public delegate_base<T1, Result> {
typedef Result (Class::* MethodType)(T1);
Class* obj;
MethodType method;
delegate_impl(Class* obj_, MethodType method_) :
obj(obj_), method(method_) { }
Result operator()(T1 v1) {
return (obj->*method)(v1);
}
};
template <typename T1, typename Result>
struct delegate {
// Notice the type: delegate_base<T1,
// Result> - no Class in sight!
delegate_base<T1, Result>* pDelegateImpl;
// The templated constructor - The presence of Class
// does not "pollute" the class itself
template <typename Class, typename T1, typename Result>
delegate(Class* obj, Result (Class::* method)(T1))
: pDelegateImpl(new delegate_impl<Class,
T1, Result>(obj, method)) {
pDelegateImpl->addref(); // Added 2002-09-22
}
// Copy constructor and assignment operator
// added 2002-09-27
delegate(const delegate<T1,
Result>& other) {
pDelegateImpl = other.pDelegateImpl;
pDelegateImpl->addref();
}
delegate<T1, Result>& operator=(const delegate<T1,
Result>& other) {
pDelegateImpl->release();
pDelegateImpl = other.pDelegateImpl;
pDelegateImpl->addref();
return *this;
}
~delegate() { pDelegateImpl->release();
} // Added & modified 2002-09-22
// Forward the delegate to the delegate implementation
Result operator()(T1 v1) {
return (*pDelegateImpl)(v1);
}
};
这样,.NET 委托的要求就满足了!有关如何实际使用委托的信息,请参阅本文顶部提供的演示源代码。
为什么?
因为我认为委托可能非常强大,而且我个人喜欢工具箱里有强大的工具。它们可能有一天会有用!
更新
- 2002-09-22:移除了内存泄漏
- 2002-09-22:修复内存泄漏引入了一个更严重的问题:悬空指针。使用 COM 式的引用计数来解决。
- 2002-09-27:添加了复制构造函数和赋值运算符