C++中類成員函數作為回調函數
背景
實現了一個C的組件以后,用在QT中,發現有點問題。為了解決調用成員函數作為回調函數,而又不想改成信號槽。特此學習了別人的做法。
前言
回調函數是基於C編程的Windows SDK的技術,不是針對C++的,程序員可以將一個C函數直接作為回調函數,但是如果試圖直接使用C++的成員函數作為回調函數將發生錯誤,甚至編譯就不能通過。
普通的C++成員函數都隱含了一個傳遞函數作為參數,亦即“this”指針,C++通過傳遞一個指向自身的指針給其成員函數從而實現程序函數可以訪問C++的數據成員。這也可以理解為什么C++類的多個實例可以共享成員函數但是確有不同的數據成員。由於this指針的作用,使得將一個CALLBACK型的成員函數作為回調函數安裝時就會因為隱含的this指針使得函數參數個數不匹配,從而導致回調函數安裝失敗。
這樣從理論上講,C++類的成員函數是不能當作回調函數的。但我們在用C++編程時總希望在類內實現其功能,即要保持封裝性,如果把回調函數寫作普通函數有諸多不便。經過網上搜索和自己研究,發現了幾種巧妙的方法,可以使得類成員函數當作回調函數使用。
這里采用Linux C++中線程創建函數pthread_create舉例,其原型如下:
int pthread_create( pthread_t *restrict tidp , const pthread_attr_t *restrict attr , void* (*start_rtn)(void*) , void *restrict arg );
這里我們只關注第三個參數start_run,它是一個函數指針,指向一個以void*
為參數,返回值為void*
的函數,這個函數被當作線程的回調函數使用,線程啟動后便會執行該函數的代碼。
做法
方法1
思路:回調函數為普通函數,但在函數體內執行成員函數。
見以下代碼:
class MyClass
{
pthread_t TID;
public:
void func()
{
//子線程執行代碼
}
bool startThread()
{//啟動子線程
int ret = pthread_create( &TID , NULL , callback , this );
if( ret != 0 )
return false;
else
return true;
}
};
static void* callback( void* arg )
{//回調函數
((MyClass*)arg)->func();調用成員函數
return NULL;
}
int main()
{
MyClass a;
a.startThread();
}
類MyClass需要在自己內部開辟一個子線程來執行成員函數func()中的代碼,子線程通過調用startThread()成員函數來啟動。這里將回調函數callback寫在了類外面,傳遞的參數是一個指向MyClass對象的指針(在pthrad_create()中由第4個參數this指定),回調函數經過強制轉換把void變為MyClass,然后再調用arg->func()執行子線程的代碼。
這樣做的原理是把當前對象的指針當作參數先交給一個外部函數,再由外部函數調用類成員函數,以外部函數作為回調函數,但執行的是成員函數的功能,這樣相當於在中間作了一層轉換。缺點是回調函數在類外,影響了封裝性,這里把callback()限定為static,防止在其它文件中調用此函數。
方法2
思路:回調函數為類內靜態成員函數,在其內部調用成員函數。
在方法1上稍作更改,把回調函數搬到類MyClass里,這樣就保持了封裝性。
代碼如下:
class MyClass
{
static MyClass* CurMy;//存儲回調函數調用的對象
static void* callback(void*);//回調函數
pthread_t TID;
void func()
{
//子線程執行代碼
}
void setCurMy()
{//設置當前對象為回調函數調用的對象
CurMy = this;
}
public:
bool startThread()
{//啟動子線程
setCurMy();
int ret = pthread_create( &TID , NULL , MyClass::callback , NULL );
if( ret != 0 )
return false;
else
return true;
}
};
MyClass* MyClass::CurMy = NULL;
void* MyClass::callback(void*)
{
CurMy->func();
return NULL;
}
int main()
{
MyClass a;
a.startThread();
}
類MyClass有了1個靜態數據成員CurMy和1個靜態成員函數callback。CurMy用來存儲一個對象的指針,充當方法一中回調函數的參數arg。callback當作回調函數,執行CurMy->func()的代碼。每次建立線程前先要調用setCurMy()來讓CurMy指向當前自己。
這個方法的好處時封裝性得到了很好的保護,MyClass對外只公開一個接口startThread(),子線程代碼和回調函數都被設為私有,外界不可見。另外沒有占用callback的參數,可以從外界傳遞參數進來。但每個對象啟動子線程前一定要注意先調用setCurMy()讓CurMy正確的指向自身,否則將為其它對象開啟線程,這樣很引發很嚴重的后果。
方法3
思路:對成員函數進行強制轉換,當作回調函數。
代碼如下:
// 關鍵的定義在這里。
class MyClass;
union for_callback {
void *(*fun_in_c)(void *);
void *(MyClass::*fun_in_class)(void);
};
class MyClass
{
int ret;
pthread_t TID;
public:
union for_callback fp;
void *func(void)
{
cout << "fun_in_class" << endl;
cout << this->ret << endl;
return NULL;
}
bool startThread()
{
fp.fun_in_class = &MyClass::func;
ret = pthread_create(&TID, NULL, fp.fun_in_c, this); // 創建線程
return true;
}
};
這個方法是原理是, MyClass::func
最終會轉化成 void func(MyClass *this);
也就是說在原第一個參數前插入指向對象本身的this指針。
可以利用這個特性寫一個非靜態類成員方法來直接作為線程回調函數。
對編譯器而言,void (MyClass::*FUNC1)(void)
和void* (*FUNC)(void*)
這兩種函數指針雖然看上去很不一樣,但他們的最終形式是相同的,因此就可以把成員函數指針強制轉換成普通函數的指針來當作回調函數。
在建立線程時要把當前對象的指針this當作參數傳給回調函數(成員函數func),這樣才能知道線程是針對哪個對象建立的。
方法三的封裝性比方法二更好,因為不涉及多個對象共用一個靜態成員的問題,每個對象可以獨立地啟動自己的線程而不影響其它對象。
方法4
使用C++的TR1中中包含一個function模板類和bind模板函數。使用它們可以實現類似函數指針的功能,但是比函數指針更加靈活。
對於tr1::function對象可以這么理解:它能接受任何可調用物,只要可調用物的的簽名式兼容於需求端即可,比如函數指針,仿函數對象,成員函數指針,
例子如下:
#include <iostream>
#include <functional>//為了使用std::tr1::function
#include <string>
#include <sstream>
using namespace std;
typedef tr1::function< int (const string&) > FUNC;
void InterfaceFunc( const string& a , const string& b , FUNC f )
{//測試用接口函數,將a+b得到的字符串轉成整數並打印出來,f是回調函數
cout << f(a+b) << endl;
}
先自定義了一種function類型FUNC,它接受所有參數為const string&並且返回值是 int的簽名對象。
函數InterfaceFunc第三個參數是一個FUNC對象,當作回調函數使用
下面是四種常見類型的“可調用物”:
int f1( const string& str )
{//正常函數
cout << "int f1( const string& str )" << endl;
stringstream ss;
ss << str;
int result;
ss >> result;
return result;
}
class F2
{
public:
int operator()( const string& str )
{//仿函數
cout << "int F2::operator()( const string& str )" << endl;
stringstream ss;
ss << str;
int result;
ss >> result;
return result;
}
};
class F3
{
public:
int f3( const string& str )
{//類內非靜態成員函數
cout << "int F3::f3( const string& str )" << endl;
stringstream ss;
ss << str;
int result;
ss >> result;
return result;
}
};
class F4
{
public:
static int f4( const string& str )
{//類內靜態成員函數
cout << "static int F4::f4( const string& str )" << endl;
stringstream ss;
ss << str;
int result;
ss >> result;
return result;
}
};
這些函數都具有形如int (const string& )形式的簽名式,所以都可以被FUNC對象接受。
具體調用的時候是這樣的:
int main()
{
string a = "123";
string b = "456";
//FUNC接受正常函數指針
InterfaceFunc( a , b , f1 );
cout << endl;
//FUNC接受仿函數
InterfaceFunc( a , b , F2() );
cout << endl;
//FUNC接受類內非靜態成員函數
F3 f;
InterfaceFunc( a , b , tr1::bind( &F3::f3 , &f , tr1::placeholders::_1 ) );
cout << endl;
//FUNC接受類內靜態成員函數
InterfaceFunc( a , b , F4::f4 );
system("pause");
}
這里需要特別注意下,第三次讓FUNC接受類內非靜態成員函數時,使用了tr1::bind( &F3::f3 , &f , tr1::placeholders::_1 )
這樣東西作為參數。
它的含義是:讓&f
作為F3::f3
函數中的第1個參數,因為對於類內非靜態成員函數,它有一個隱含的第一參數:this指針,因為成員函數是存儲位置是在對象之外的,只根據F3::f3的地址無法得知具體對哪個對象操作,tr1::bind
的作用正是告訴F3::f3
,它隱含的this參數指向的是f
這個已經定義好的對象,剩下的事情就和其它情況一樣了。