C++ 中的委托,带自然赋值运算符






2.86/5 (7投票s)
类似于 C# 或 Delphi 的委托类型。
关于文章示例
本文附件中的示例是一个非常简单的例子,但它旨在展示事件类使用的简洁性以及与在其他编程语言中触发事件的相似性。它包含一个标准的 MFC 对话框,带有两个 CBtnWithEvent
类型的按钮控件。每个按钮都有一个内置的 Click
事件,类型为 NotifyEvent
,这是 QProcedure<CWnd*, int>
的别名。当您按下任何一个按钮时,它的 DoClick
方法将被调用,OnOkClick
或 OnCancelClick
对话框方法将被触发,程序将终止。
用法
一切都始于 CEventsDemoDlg::OnInitDialog
方法,在该方法中,按钮被附加到 HWND
(可以通过多种方式完成;例如,在 CDialog::DoDataExchange
方法中),并且 Click
事件被分配给对话框上的处理程序方法。
// in header files
// typedef QProcedure<CWnd*, int> NotifyEvent;
// public: NotifyEvent CBtnWithEvent::Click
// public: CBtnWithEvent CEventsDemoDlg::buttonOK
// public: CBtnWithEvent CEventsDemoDlg::buttonCancel
BOOL CEventsDemoDlg::OnInitDialog() {
/* */
buttonOK.Attach(GetDlgItem(IDOK)->m_hWnd);
buttonOK.Click = &CEventsDemoDlg::OnOkClick, this;
buttonCancel.Attach(GetDlgItem(IDCANCEL)->m_hWnd);
buttonCancel.Click = &CEventsDemoDlg::OnCancelClick, this;
return TRUE;
}
接下来,按下任意一个按钮后,其 DoClick
方法将从重写的 CButton::OnChildNotify
方法中被调用。
void CButtonWithEvent::DoClick() {
if (Click != NULL)
Click(this, GetWindowLong(m_hWnd, GWL_ID));
}
最后,会调用一个合适的方法处理程序。
void CEventsDemoDlg::OnOkClick(CWnd* sender, int nID)
{
ASSERT( nID == IDOK );
EndDialog(nID);
}
void CEventsDemoDlg::OnCancelClick(CWnd* sender, int nID)
{
ASSERT( nID == IDCANCEL );
EndDialog(nID);
}
引言
多年前,我需要将一个用 Delphi 编写的数据库应用程序移植到 C++。任务很简单,但有一个小细节:C++ 没有内置的委托。当然,C++ 语言提供了指向成员函数的指针,但是通过指针调用成员函数需要传递要调用这些函数的对象指针。在许多情况下,使用成员函数指针比使用类型强制转换来应用它们更灵活。所以我决定自己实现一个委托类型。在本文中,我将尝试向您解释我是如何做到的。
文件内容
qevents.h 提供了四个类:QProcedure
和 QFunction
是严格的委托类,它们的共同祖先是 QCustomEvent
类,还有一个简单的 QEException
类用于识别前面提到的类的异常。QProcedure
和 QFunction
都是模板类,拥有零到十个默认的虚拟参数类型 _yryQQ6*
,这些参数在编译时会被替换为适当的成员函数参数。所有这些类都已打包在 properties
命名空间中。
“this”指针
调用成员函数时,“this”指针非常重要,因为它是类字段和在函数体内部调用的其他方法的默认上下文。但我们可以非常轻松地更改这个指针,因为编译器只检查将要调用该方法的类型。例如:
class SomeClass
{
int m_field;
void f(void*) { }
int g(int, int, int) { return 0; }
};
class AnotherClass {
int m_field;
public:
void f(void* param)
{
bool this_is_not_null = (this != NULL);
bool this_like_param = (this == param);
m_field = 0; // - when param is AnotherClass* then is natural
// - when param is SomeClass* then is ok, because
// SomeClass has field with int width at the same offset
// - when param is NULL then will be boom
}
void g()
{
SomeClass sc;
AnotherClass ac;
void (AnotherClass::*meth)(void*) = &AnotherClass::f;
// 1
(this->*meth)(this); // is ok
// 2
(&ac->*meth)(&ac); // is ok also
// 3
(&sc->*meth)(&sc); // error: &cs is not of AnotherClass*
// 4
(((AnotherClass*)&sc)->*meth)(this); // dangerous
// 5
(((AnotherClass*)0)->*meth)(this); // dangerous
}
};
上面的示例显示了通过指针调用成员函数,编译器只严格检查方法签名;但是,我们可以通过简单地类型转换为所需的类型来轻松欺骗该指针指向将调用此方法的对象。在 AnotherClass::f
中,对于第一次和第二次调用,this_is_not_null
和 this_like_param
都将为 true
,但在第四次调用中,this_like_param
将为 false
,在第五次调用中,“this”指针将为 NULL
。每个方法的代码实际上都嵌入在运行的应用程序中。这些不是动态方法,每个方法在执行期间都只有一个二进制代码块,无论实现了这些方法的类型的实例有多少。我们只在调用它们时更改它们的上下文(“this”指针)。
更改指向方法的指针类型
在 C++ 中,不仅“this”指针可以通过类型转换来更改。我可以说几乎所有东西都可以。对我们来说最有趣的是指向方法的指针类型。在前面的示例中,AnotherClass::g
中的第三次调用会导致编译错误,但它应该这样写:
// previous code from AnotherClass::g()
// (&sc->*meth)(&sc); // error: &cs is not of AnotherClass*
// with type casting now is available
(&sc->*( (void (SomeClass::*)(void*)) meth ))(&sc);
当然,这不是一个非常易读的语法;下面的会更易读。
typedef void (SomeClass::* SCM)(void*);
typedef void (AnotherClass::* ACM)(void*);
void f()
{
SomeClass sc, *psc = ≻
ACM ac_meth = &AnotherClass::f;
(psc->*((SCM)ac_meth))(psc);
}
类型转换成员函数指针仍然存在一个危险。让我们考虑以下情况:
void g()
{
SomeClass sc, *psc = ≻
int (SomeClass::* g_ptr)(int,int,int) = &SomeClass::g;
(psc->*((SCM)g_ptr))(psc);
}
乍一看,一切都很好。但是,当控制流到达调用 g_ptr
时,它将以 *SCM* 语义被调用。SomeClass::g
的结果意义不大。错误在别处,因为此方法需要三个参数,但只获得了一个,并且发生了栈回溯问题。确切地说,在这种调用中发生了什么是一个另一个文章的主题,所以我现在就不深入讨论了。我们必须记住,这样的技巧非常危险,并且总会在运行时导致错误。
方法模板
让我们稍微看一下方法指针变量赋值的右侧。没有类型转换,它仍然会导致编译错误,并且必须隐式进行转换——更方便。
SCM sc_meth = &AnotherClass::f; // causes compilation error
SCM sc_meth = (SCM)&AnotherClass::f; // is ok
但是,当我们使用模板方法与匿名类的方法指针参数时会发生什么?它可以这样写:
class AnotherClass
{
/* */
public:
template<typename CLASS> void h( void (CLASS::*)(void*) ) { }
}
现在可以使用以下语法:
void i()
{
AnotherClass ac;
SCM sc_meth = &SomeClass::f;
ACM ac_meth = &AnotherClass::f;
ac.h(sc_meth);
ac.h(ac_meth);
}
在上面两种情况中,当调用 AnotherClass::h
时,编译器会识别出拥有没有返回类型且参数类型为 void*
的方法的类型 CLASS
,在第一种情况下是 AnotherClass
类,在第二种情况下是 SomeClass
。我们用一个参数(类型为 void*
)和隐式返回类型定义了 AnotherClass::h
模板方法。在稍微重构我们的方法后,它可以接受任何类型的参数,更改在于添加两个模板参数:一个用于返回类型,一个用于方法参数类型。这是代码:
class AnotherClass
{
/* */
public:
template<class RES, class CLASS, class PAR>
void j(RES (CLASS::*mptr)(PAR)) { }
void k()
{
SCM sc_meth = &SomeClass::f;
ACM ac_meth = &AnotherClass::f;
j(sc_meth);
j(sc_meth);
}
}
看起来我们什么都没得到。但事实恰恰相反。在 AnotherClass::j
的主体中,我们拥有关于 mptr
方法指针的完整信息:其返回类型在 RES
模板参数中,其参数类型在 PAR
中,以及拥有它的类的类型在 CLASS
中。
类模板
随着方法模板一起的是类模板。借助类模板,我们能够为我们尚不知道的类型实现行为。类模板与方法模板一样可以有一个或多个模板参数,但与方法模板不同的是,这些参数可以有默认值。现在,我将展示如何使用类模板获得与 AnotherClass::j
方法相同的结果。我即兴创作了前面的例子:
template<typename RES, typename PAR>
class MethodWithOneParameterHandler
{
typedef RES (MethodWithOneParameterHandler::*MPTR)(PAR);
MPTR m_mptr;
public:
template<typename CLASS> const MethodWithOneParameterHandler&
operator=(RES (CLASS::*mptr)(PAR)) {
m_mptr = (MPTR)mptr;
return *this;
}
};
/* */
void l()
{
MethodWithOneParameterHandler<void, void*> handler;
handler = &SomeClass::f;
handler = &AnotherClass::f;
}
简单的 MethodWithOneParameterHandler
类实现了一个非常重要的事情,即它省略了分配方法指针给它的类的相关信息,并同时隐藏了这些信息。在实际的委托中,这些信息是透明的,我们不需要担心它。我们需要记住使用简单的类型转换来为我们的字段(这里是 m_mptr
)分配方法指针值。
在示例中,方法的返回类型是 void
,这不能作为返回类型。但这里有一点解释。类模板和方法模板被视为独立的参数,因此在这种意义上,void
与任何其他类型具有相同的权利。从委托的定义来看,类型是已知的,并且参数可以根据委托类型而变化。为了扩展列表,所检查的类型只需要添加目标数量的参数,并为每个参数添加重载的赋值运算符。为了使这个类和最终的委托结果不依赖于其类型,我们只需要为每个参数赋予默认值,即类型名称。这个默认类型可以是任何类型,例如 int
或 double
,但最好是指针类型。这是想法说明:
typedef struct IMAGINE_TYPE { int unused; } *IMGT;
template<class RES, class P1=IMGT, class P2=IMGT, class P3=IMGT>
class MethodWithMaxThreParsHandler {
typedef RES (MethodWithMaxThreParsHandler::*MPTR_P0)();
typedef RES (MethodWithMaxThreParsHandler::*MPTR_P1)(P1);
typedef RES (MethodWithMaxThreParsHandler::*MPTR_P2)(P1,P2);
typedef RES (MethodWithMaxThreParsHandler::*MPTR_P3)(P1,P2,P3);
union { MPTR_P0 m0; MPTR_P1 m1; MPTR_P2 m2; MPTR_P3 m3; } m_mptr;
int parameter_count;
public:
template<typename CLASS> const MethodWithMaxThreParsHandler&
operator=(RES (CLASS::*mptr)()) {
m_mptr.m0 = (MPTR_P0)mptr;
parameter_count = 0;
return *this;
}
template<typename CLASS> const MethodWithMaxThreParsHandler&
operator=(RES (CLASS::*mptr)(P1)) {
m_mptr.m1 = (MPTR_P1)mptr;
parameter_count = 1;
return *this;
}
template<typename CLASS> const MethodWithMaxThreParsHandler&
operator=(RES (CLASS::*mptr)(P1, P2)) {
m_mptr.m2 = (MPTR_P2)mptr;
parameter_count = 2;
return *this;
}
template<typename CLASS> const MethodWithMaxThreParsHandler&
operator=(RES (CLASS::*mptr)(P1, P2, P3)) {
m_mptr.m3 = (MPTR_P3)mptr;
parameter_count = 3;
return *this;
}
};
调用赋值运算符后,我们就可以检查调用了哪个重载运算符,因此 mptr
会被正确转换,并且实际的方法指针参数数量存储在 parameter_count
字段中。为了降低成本,m_mptr
是一个联合体,因为指针 MPTR_P<0|1|2|3>
具有相同的大小。准备好的类可以如下使用:
class SomeClass {
/* */
public:
char* m_with_1_par(char) { return NULL; }
long m_with_3_par(short, int, long) { return 0; }
};
class AnotherClass {
/* */
public:
int m_with_0_par() { return 0; }
double m_with_2_par(float, double) { return 0.0; }
};
void m()
{
MethodWithMaxThreParsHandler<int> with_0_parameters;
MethodWithMaxThreParsHandler<char*, char> with_1_parameters;
MethodWithMaxThreParsHandler<double, float, double> with_2_parameters;
MethodWithMaxThreParsHandler<long, short, int, long> with_3_parameters;
with_0_parameters = &AnotherClass::m_with_0_par;
with_1_parameters = &SomeClass::m_with_1_par;
with_2_parameters = &AnotherClass::m_with_2_par;
with_3_parameters = &SomeClass::m_with_3_par;
}
传递对象指针
传递对象指针以便为其触发事件有许多方法。它可以简单地在方法参数中传递对象指针,或将其分配给某个委托字段,但这些操作需要额外的调用。为了进行单行赋值,它可以是任何带有两个参数的运算符,其中第一个将是委托类型,例如逗号运算符。这是一个示例代码:
template<class RES, class P1=IMGT, class P2=IMGT, class P3=IMGT>
class MethodWithMaxThreParsHandler {
/* */
private:
MethodWithMaxThreParsHandler* _p_this;
MethodWithMaxThreParsHandler* get_this() {
if (_p_this == NULL)
throw;
return _p_this;
}
public:
MethodWithMaxThreParsHandler() {
_p_this = NULL;
parameter_count = (-1);
}
friend void operator,(const MethodWithMaxThreParsHandler &e, void* p) {
((MethodWithMaxThreParsHandler*)&e)->_p_this = (MethodWithMaxThreParsHandler*)p;
}
};
在这里,在 MethodWithMaxThreParsHandler
类的修改中,通过逗号运算符传递的对象指针被存储在私有的 _p_this
字段中。这个指针将用作以后在触发事件时传递给处理程序方法的“this”指针,因此,为了开始,其值暂时设为 NULL
,以便在 get_this
方法中稍后检查。
void n()
{
SomeClass sc;
AnotherClass ac;
MethodWithMaxThreParsHandler<int> with_0_parameters;
MethodWithMaxThreParsHandler<char*, char> with_1_parameters;
MethodWithMaxThreParsHandler<double, float, double> with_2_parameters;
MethodWithMaxThreParsHandler<long, short, int, long> with_3_parameters;
with_0_parameters = &AnotherClass::m_with_0_par, ∾
with_1_parameters = &SomeClass::m_with_1_par, ≻
with_2_parameters = &AnotherClass::m_with_2_par, ∾
with_3_parameters = &SomeClass::m_with_3_par, ≻
}
触发事件
为了增加通过函数触发事件的可能性,最好的选择是函数运算符。此运算符以及每个方法都可以重载。有了它,我们就可以调用之前分配给委托变量的任何方法。
template<class RES, class P1=IMGT, class P2=IMGT, class P3=IMGT>
class MethodWithMaxThreParsHandler {
/* */
public:
RES operator()() {
if (parameter_count != 0) throw;
return (get_this()->*m_mptr.m0)();
}
RES operator()(P1 p1) {
if (parameter_count != 1) throw;
return (get_this()->*m_mptr.m1)(p1);
}
RES operator()(P1 p1, P2 p2) {
if (parameter_count != 2) throw;
return (get_this()->*m_mptr.m2)(p1, p2);
}
RES operator()(P1 p1, P2 p2, P3 p3) {
if (parameter_count != 3) throw;
return (get_this()->*m_mptr.m3)(p1, p2, p3);
}
};
不幸的是,在这种方法中,可能会调用任何重载的函数运算符,因此对于带有两个参数的方法,它可以被调用零个、一个或任意数量的参数。但幸运的是,适当数量的参数可以存储在 parameter_count
字段中,我们可以在调用目标方法之前检查其值。另一方面,事件很少或几乎从其所有者外部触发。程序员的任务是提供一个方法处理程序,而无需关心该处理程序如何以及何时被调用。现在我们可以编写以下代码:
void m()
{
SomeClass sc;
AnotherClass ac;
MethodWithMaxThreParsHandler<int> with_0_parameters;
MethodWithMaxThreParsHandler<char*, char> with_1_parameters;
MethodWithMaxThreParsHandler<double, float, double> with_2_parameters;
MethodWithMaxThreParsHandler<long, short, int, long> with_3_parameters;
/*
* Remember about adding pointer to
* calling object with comma operator
*/
with_0_parameters = &AnotherClass::m_with_0_par, ∾
with_1_parameters = &SomeClass::m_with_1_par, ≻
with_2_parameters = &AnotherClass::m_with_2_par, ∾
with_3_parameters = &SomeClass::m_with_3_par; // oops, object is missing
int res0 = with_0_parameters();
char* res1 = with_1_parameters('a');
double res2 = with_2_parameters(0.3f, 3.14);
long res3 = with_3_parameters(0, 1, 2); // will be boom from get_this
// method at runtime
IMGT dummy = NULL;
res0 = with_0_parameters(0, dummy, dummy); // exception: delegate has 0 parameters
res1 = with_1_parameters('b', NULL, dummy); // exception: delegate has 1 parameters
res2 = with_2_parameters(0.1f); // exception: delegate has 2 parameters
res3 = with_3_parameters(10, 11); // exception: delegate has 3 parameters
}
在大多数情况下,是编译器发现类型转换错误,例如,*0* 和 NULL
都是正确的值,直到运行时我们都不会遇到异常。
免责声明
本软件及随附文件按“原样”分发,不附带任何明示或暗示的保证。对于可能的损害甚至功能,不承担任何责任。用户必须承担使用本软件的全部风险。