您的位置:首页 > 编程语言 > C语言/C++

C++类成员函数作为回调函数(提供完整工程代码)

2017-06-28 23:33 288 查看

遇见

在我们使用一些第三方SDK、不同框架层之间的事件通知或第三方的C库时,经常需要用相应的接口来注册一些回调函数来处理特定的事件。这里以NOLO SDK为例来描述这样的一个问题。

typedef enum ExpandMsgType {

DoubleClickMenu=1,
DoubleClickSystem
}ExpandMsgType;

typedef void(__cdecl *expandMsg_FuncCallBack)(ExpandMsgType expandMsgType);
NOLO_API  bool _cdecl expandDataNotify_FuncCallBack(expandMsg_FuncCallBack fun);


我们注册一个expandMsg_FuncCallBack类型的回调函数,代码如下:

#include "stdafx.h"
#include <windows.h>
#include "nolo_api.h"
using namespace NOLO;
class HeadMountDevice{
public:
HeadMountDevice() {};
~HeadMountDevice() {};
void CallBackFunction(ExpandMsgType expandMsgType) {};
};
int main()
{
HeadMountDevice *p_hmd = new HeadMountDevice();
expandDataNotify_FuncCallBack(p_hmd->CallBackFunction);
system("Pause");
return 0;
}


报错:

error C3867: 'HeadMountDevice::CallBackFunction': non-standard syntax; use '&' to create a pointer to member


似乎是需要使用’&’来指向成员函数,修改后如下:

#include "stdafx.h"
#include <windows.h>
#include "nolo_api.h"
using namespace NOLO;
class HeadMountDevice{
public:
HeadMountDevice() {};
~HeadMountDevice() {};
void CallBackFunction(ExpandMsgType expandMsgType) {};
};
int main()
{
HeadMountDevice *p_hmd = new HeadMountDevice();
expandDataNotify_FuncCallBack(&p_hmd->CallBackFunction);//modify this line
system("Pause");
return 0;
}


还是报错:

error C2276: '&': illegal operation on bound member function expression


使用”&类名::函数名”方式访问成员函数的地址:

...
expandDataNotify_FuncCallBack(&HeadMountDevice::CallBackFunction);
...


依然报错:

error C2664: 'bool NOLO::expandDataNotify_FuncCallBack(NOLO::expandMsg_FuncCallBack)': cannot convert argument 1 from 'void (__thiscall HeadMountDevice::* )(NOLO::ExpandMsgType)' to 'NOLO::expandMsg_FuncCallBack'


分析

这里成员函数作为回调函数,不能通过这种方式来指定,主要原因有两个:

1. 成员函数属于类本身,而不属于任何一个特定的实体.

2. 成员函数默认隐藏了一个”this”指针参数,填充在参数列表的第一个位置,所以你的回调函数与SDK上的回调函数实际上并不匹配!

上面第一点,很容易理解,关于第二点这里引用Joseph Garvin在这里的解释.

class A {
public:
A() : data(0) {}
void foo(int addToData) { this->data += addToData; }

int data;
};

...

A an_a_object;
an_a_object.foo(5);
A::foo(&an_a_object, 5); // This is the same as the line above!
std::cout
/*
How many parameters does A::foo take? Normally we would say 1. But under the hood, foo really takes 2. Looking at A::foo's definition,
it needs a specific instance of A in order for the 'this' pointer to be meaningful (the compiler needs to know what 'this' is). The way
you usually specify what you want 'this' to be is through the syntax MyObject.MyMemberFunction(). But this is just syntactic sugar for
passing the address of MyObject as the first parameter to MyMemberFunction. Similarly when we declare member functions inside class definitions
we don't put 'this' in the parameter list, but this is just a gift from the language designers to save typing. Instead you have to specify
that a member function is static to opt out of it automatically getting the extra 'this' param.
*/


上面这段话主要是说明了成员函数在调用时,默认都会将调用成员函数的对象作为参数一起传入,同时C++成员函数是按thiscall的调用规则,即参数从右到左压入栈中,且this指针存放在CX寄存器.这里做了验证实验,代码如下:

// 类成员函数作为回调函数.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <windows.h>
#include "nolo_api.h"
using namespace NOLO;
#pragma comment(lib,"noloRuntime.lib")
class HeadMountDevice{
public:
HeadMountDevice() {};
~HeadMountDevice() {};
void CallBackFunction(ExpandMsgType expandMsgType) {//-------------------------break1-1
};
};
void GlobalCallBackFunction(ExpandMsgType expandMsgType) {};//---------------------break2-0
int main()
{
HeadMountDevice *p_hmd = new HeadMountDevice();
ExpandMsgType msg_type = DoubleClickMenu;
GlobalCallBackFunction(msg_type);
p_hmd->CallBackFunction(msg_type);//-------------------------------------------break1-0
//expandDataNotify_FuncCallBack(&p_hmd->CallBackFunction);
/*
register global function is ok.
expandDataNotify_FuncCallBack(GlobalCallBackFunction);
*/
system("Pause");
return 0;
}


其中断点”break1-0”,记录的’p_hmd’指针地址为’0x0086f590’:



断点”break1-1”,对应的汇编代码如下,并且可以看到参数压栈的最后一个是ecx,ecx的值就是p_hmd指针的地址!即在调用成员函数时,默认会将this指针作为参数列表的第一个参数(入栈顺序是从右向左,所以最后入栈)压入堆栈中.



而断点”break2-0”全局函数运行时的汇编代码如下(没有压入ECX):

void GlobalCallBackFunction(ExpandMsgType expandMsgType) {};
00DB4B20  push        ebp
00DB4B21  mov         ebp,esp
00DB4B23  sub         esp,0C0h
00DB4B29  push        ebx
00DB4B2A  push        esi
00DB4B2B  push        edi
00DB4B2C  lea         edi,[ebp-0C0h]


通过上面汇编代码,找到出错的原因,下面是解决办法.

解决办法

解决办法1:回调函数为全局函数.

// 类成员函数作为回调函数.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <windows.h>
#include "nolo_api.h"
#include <iostream>
#include <functional>
using namespace NOLO;
#pragma comment(lib,"noloRuntime.lib")
class HeadMountDevice{
public:
HeadMountDevice() {};
~HeadMountDevice() {};
void CallBackFunction(ExpandMsgType expandMsgType) {
};
};
//global function
void GlobalCallBackFunction(ExpandMsgType expandMsgType) {};
int main()
{
HeadMountDevice *p_hmd = new HeadMountDevice();
ExpandMsgType msg_type = DoubleClickMenu;
GlobalCallBackFunction(msg_type);
//p_hmd->CallBackFunction(msg_type);
//expandDataNotify_FuncCallBack(&p_hmd->CallBackFunction);

//    register global function is ok.
expandDataNotify_FuncCallBack(GlobalCallBackFunction);

system("Pause");
return 0;
}


全局函数的方法不会有This指针的问题,可以满足某些需求,但是缺点很明显,访问类的成员将会非常麻烦,封装性差.

解决办法2:将要设置为回调函数的成员函数定义为静态成员函数

#include "stdafx.h"
#include <windows.h>
#include "nolo_api.h"
#include <iostream>
#include <functional>
using namespace NOLO;
#pragma comment(lib,"noloRuntime.lib")
class HeadMountDevice{
public:
HeadMountDevice() {};
~HeadMountDevice() {};
static void CallBackFunction(ExpandMsgType expandMsgType) {};//static member function
};
int main()
{
HeadMountDevice *p_hmd = new HeadMountDevice();
expandDataNotify_FuncCallBack(p_hmd->CallBackFunction);
system("Pause");
return 0;
}


虽然可以通过编译和使用,但是这样做的缺点很明显,静态成员函数只能访问静态成员变量!这个限制在多数情况下,是不能够接受的.

解决办法3:办法2的改进版,解决方法2对非静态成员/函数的访问限制.

// 类成员函数作为回调函数.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <windows.h>
#include "nolo_api.h"
#include <iostream>
#include <functional>
using namespace NOLO;
#pragma comment(lib,"noloRuntime.lib")
class HeadMountDevice{
public:
HeadMountDevice() {
};
~HeadMountDevice() {};
void CallBackFunction(ExpandMsgType expandMsgType) {
printf("msg_type:%d\n", expandMsgType);
};
static void BridgeToCallBack(ExpandMsgType expandMsgType) {
m_pThis->CallBackFunction(expandMsgType);
}
void SetThisPointer(HeadMountDevice *p_this) {
m_pThis = p_this;
}
private:
static HeadMountDevice *m_pThis;
};
//static member need to define for allocated memory , otherwise will cause compile error!
HeadMountDevice * HeadMountDevice::m_pThis = NULL;
//global function
void GlobalCallBackFunction(ExpandMsgType expandMsgType) {};
int main()
{
HeadMountDevice *p_hmd = new HeadMountDevice();
p_hmd->SetThisPointer(p_hmd);
expandDataNotify_FuncCallBack(HeadMountDevice::BridgeToCallBack);
system("Pause");
return 0;
}


方法3应该是目前比较合适的解决方案,在这个问题时有打算通过std::bind和std::function,或boost::bind和boost::function来实现这个功能,通过bind之后的函数,可以执行非静态的类成员函数,但是不能够被注册为回调函数,会报类似this指针的错,在无法修改SDK接口的情况下,应该是实现不了的,当然如果可以实现,麻烦告诉我一下.

最后

这里提供两个可以调试的工程:一个工程是简单版本的SDK,另外一个工程是使用此SDK注册C回调函数的完整工程.

SDK的工程

SDK_DEMO.h:

#pragma once
#include <windows.h>
#ifndef SDK_DEMO_API
#define SDK_DEMO_API    extern "C"
#else
#define SDK_DEMO_API
#endif
namespace SDK_DEMO {
typedef void(__cdecl *SdkCallBack)(int num);
SDK_DEMO_API void _cdecl SdkInit();
SDK_DEMO_API void _cdecl SdkRun();
SDK_DEMO_API void _cdecl SdkExit();
SDK_DEMO_API bool _cdecl RegistetSdkCallBack(SdkCallBack call_back);
};


SDK_DEMO.cpp:

// SDK_DEMO.cpp : Defines the exported functions for the DLL application.
//
#include "stdafx.h"
#include "sdk_demo.h"
#include <stdio.h>
namespace SDK_DEMO {
SdkCallBack g_SdkCallBack = NULL;
SDK_DEMO_API void _cdecl SdkInit() {
printf("%s\n",__func__);
}
SDK_DEMO_API void _cdecl SdkRun() {
printf("%s\n", __func__);
while (true)
{
if (g_SdkCallBack != NULL) {
g_SdkCallBack(5);
}
Sleep(1 * 1000);
}
}
SDK_DEMO_API void _cdecl SdkExit() {
printf("%s\n", __func__);
}
SDK_DEMO_API bool _cdecl RegistetSdkCallBack(SdkCallBack call_back) {
g_SdkCallBack = call_back;
return true;
}
};


使用SDK提供的C接口注册回调函数

member_method_as_c_callback.cpp:

// member_method_as_c_callback.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <windows.h>
#include "SDK_DEMO.h"
using namespace SDK_DEMO;
#pragma comment(lib,"SDK_DEMO.lib")
class HeadMountDevice {
public:
HeadMountDevice() {
};
~HeadMountDevice() {};
void CallBackFunction(int num) {
printf("HeadMountDevice<%s>:%d\n",__func__, num);
};
static void BridgeToCallBack(int num) {
m_pThis->CallBackFunction(num);
}
void SetThisPointer(HeadMountDevice *p_this) {
m_pThis = p_this;
}
private:
static HeadMountDevice *m_pThis;
};
//static member need to define for allocated memory , otherwise will cause compile error!
HeadMountDevice * HeadMountDevice::m_pThis = NULL;
int main()
{
HeadMountDevice *p_hmd = new HeadMountDevice();
p_hmd->SetThisPointer(p_hmd);
SDK_DEMO::SdkInit();
SDK_DEMO::RegistetSdkCallBack(HeadMountDevice::BridgeToCallBack);
SDK_DEMO::SdkRun();

system("Pause");
return 0;
}


有需要动手调试的可以直接下载调试:SDK和回调实验工程代码.

参考资料:

C++Primer Plus 6th第10章 this指针

stackoverflow:class member function as a callback

GLFW:how do i use c methods as callbacks
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息