天天看點

在C++實作回調

來看看怎麼在C++中實作回調吧。

Method1:使用全局函數作為回調

在C語言中的回調很友善。當然,我們可以在C++中使用類似于C方式的回調函數,也就是将全局函數定義為回調函數,然後再供我們調用。

typedef void(*pCalledFun)(int *);

void GetCallBack(pCalledFun parafun)

{

}

如果我們想使用GetCallBack函數,那麼就要實作一個pCalledFun類型的回調函數:

void funCallback(int *iNum)

{

}

然後,就可以直接把funCallback當作一個變量傳遞給GetCallBack,

GetCallBack(funCallback);

編譯器可能會有幾種調用規範。比如在Visual C++中,可以在函數類型前加_cdecl,_stdcall來表示其調用規範(預設為_cdecl)。調用規範影響編譯器産生的給定函數名,參數傳遞的順序(從右到左或從左到右),堆棧清理責任(調用者或者被調用者)以及參數傳遞機制(堆棧,CPU寄存器等)。看看下面的例子:

#include <iostream>

using namespace std;

typedef void (__stdcall *pFun)(void);

typedef void (__cdecl *pFunc)(void);

void __stdcall TextPrint(void)

{

cout << "Call Back Like Pascal" << endl;

}

void __cdecl TextPrintc(void)

{

cout << "Call Back Like C" << endl;

}

void ForText(pFun pFun1, pFunc pFun2)

{

pFun1();

pFun2();

}

void main(void)

{

//pFun pP = TextPrint;

//pFunc pPC = TextPrintc;

//pP();

//pPC();

ForText(TextPrint, TextPrintc);

}

Method2:使用類的靜态函數作為回調

既然使用了C++,就不能總是生活在C的陰影中,我們要使用類,類,類!!!

下面我們來使用類的靜态函數作為回調,為啥先說靜态函數,因為靜态函數跟全局函數很類似,函數調用時不會使用this指針,我們可以像用全局函數一樣使用靜态函數。如下:

#include <iostream>

using namespace std;

typedef void (*pFun)(void);

class CCallBack

{

public:

static void TextPrint(void)

{

cout << "Static Callback Function of a Class" << endl;

}

};

void ForText(pFun pFun1)

{

pFun1();

}

void main(void)

{

ForText(CCallBack::TextPrint);

}

當然,我們可以把typedef封裝到類中,加大内聚。

#include <iostream>

using namespace std;

class CCallBack

{

public:

typedef void (*pFun)(void);

static void TextPrint(void)

{

cout << "Static Callback Function of a Class with funtype" << endl;

}

};

void ForText(CCallBack::pFun pFun1)

{

pFun1();

}

void main(void)

{

ForText(CCallBack::TextPrint);

}

Method3:使用仿函數作為回調

上面兩種方法用來用去感覺還是在用C的方式,既然是C++,要面向對象,要有對象!那麼就來看看仿函數吧。所謂仿函數,就是使一個類的使用看上去象一個函數,實質就是在類中重載操作符operator(),這個類就有了類似函數的行為,就是一個仿函數類了。這樣的好處就是可以用面向對象的考慮方式來設計、維護和管理你的代碼。多的不說,見例子:

#include <iostream>

using namespace std;

typedef void(*Fun)(void);

inline void TextFun(void)

{

cout << "Callback Function" << endl;

}

class TextFunor

{

public:

void operator()(void) const

{

cout << "Callback Functionor" << endl;

}

};

void ForText(Fun pFun, TextFunor cFun)

{

pFun();

cFun();

}

void main(void)

{

TextFunor cFunor;

ForText(TextFun, cFunor);

}

援引一點關于仿函數的介紹吧:

仿函數(functor)的優點

我的建議是,如果可以用仿函數實作,那麼你應該用仿函數,而不要用回調。

原因在于:

仿函數可以不帶痕迹地傳遞上下文參數。 而回調技術通常 使用一個額外的 void*參數傳遞。這也是多數人認為回 調技術醜陋的原因。

更好的性能。 仿函數技術可以獲得更好的性能, 這點直覺來講比較難以了解。 你可能說,回調函數申明為 inline了,怎麼會性能比仿函數差?我們這裡來分析下。我們假設某個函數 func(例如上面的 std::sort)調用中傳遞了一個回調函數(如上面的 compare),那麼可以分為兩種情況:

func 是内聯函數,并且比較簡單,func 調用最終被展開了,那麼其中對回調函數的調用也成為一普通函數調用 (而不是通過函數指針的間接調用),并且如果這個回調函數如果簡單,那麼也可能同時被展開。在這種情形 下,回調函數與仿函數性能相同。

func 是非内聯函數,或者比較複雜而無法展開(例如上面的 std::sort,我們知道它是快速排序,函數因為存在遞歸而無法展開)。此時回調函數作為一個函數指針傳 入,其代碼亦無法展開。而仿函數則不同。雖然 func 本 身複雜不能展開,但是 func 函數中對仿函數的調用是編 譯器編譯期間就可以确定并進行 inline 展開的。是以在 這種情形下,仿函數比之于回調函數,有着更好的性能。 并且,這種性能優勢有時是一種無可比拟的優勢(對于 std::sort 就是如此,因為元素比較的次數非常巨大,是 否可以進行内聯展開導緻了一種雪崩效應)。

仿函數(functor)不能做的

話又說回來了,仿函數并不能完全取代回調函數所有的應用場合。例如,我在 std::AutoFreeAlloc 中使用了回調函數,而不是仿函數, 這是因為 AutoFreeAlloc 要容納異質 的析構函數,而不是隻支援某一種類的析構。這和模闆(template)不能處理在同一個容器中 支援異質類型,是一個道理。

Method4:使用類的非靜态函數作為回調(采用模闆的方法)

現在才開始說使用類的非靜态方法作為回調是這樣的,C++本身并不提供将類的方法作為回調函數的方案,而C++類的非靜态方法包含一個預設的參數:this指針,這就要求回調時不僅需要函數指針,還需要一個指針指向某個執行個體體。解決方法有幾種,使用模闆和編譯時的執行個體化及特化就是其中之一,看例子:

#include <iostream>

using namespace std;

template < class Class, typename ReturnType, typename Parameter >

class SingularCallBack

{

public:

typedef ReturnType (Class::*Method)(Parameter);

SingularCallBack(Class* _class_instance, Method _method)

{

class_instance = _class_instance;

method = _method;

};

ReturnType operator()(Parameter parameter)

{

return (class_instance->*method)(parameter);

};

ReturnType execute(Parameter parameter)

{

return operator()(parameter);

};

private:

Class* class_instance;

Method method;

};

class CCallBack

{

public:

int TextPrint(int iNum)

{

cout << "Class CallBack Function" << endl;

return 0;

};

};

template < class Class, typename ReturnType, typename Parameter >

void funTest(SingularCallBack<Class, ReturnType, Parameter> tCallBack)

{

tCallBack(1);

}

void main(void)

{

CCallBack callback;

SingularCallBack<CCallBack, int, int> Test(&callback, callback.TextPrint);

Test.execute(1);

Test(1);

funTest(Test);

}

Method5:使用類的非靜态函數作為回調(采用thunk的方法1)

所謂thunk,就是替換,改變系統本來的調用意圖,也有的說是用機器碼代替系統調用。

替換原來意圖,轉調我們需要的位址。 網上有段解釋是這樣“巧妙的将資料段的幾個位元組的資料設為特殊的值,然後告訴系統,這幾個位元組的資料是代碼(即将一個函數指針指向這幾個位元組的第一個位元組)”。

為什麼不能直接使用類的非靜态函數作為回調函數呢,通俗點的解釋就是類的非靜态函數都要預設傳入一個this指針參數,這就跟我們平時的回調不同了,是以無法使用。

上面提到過,一般的回調函數都是_stdcall或者_cdecl的調用方式,但是成員函數是__thiscall的調用方式。這種調用方式的差别導緻不能直接使用類的非靜态成員函數作為回調函數。看看差別吧:

關鍵字 堆棧清除 參數傳遞
__stdcall 被調用者 将參數倒序壓入堆棧(自右向左)
__thiscall 被調用者 壓入堆棧,this指針儲存在 ECX 寄存器中

可見兩者的不同之處就是_thiscall把this指針儲存到了ECX的寄存器中,其他都是一樣的。是以我們隻需要在調用過程中首先把this指針儲存到ECX,然後跳轉到期望的成員函數位址就可以了。代碼如下:

#include <tchar.h>

#include <wtypes.h>

#include <iostream>

using namespace std;

typedef void (*FUNC)(DWORD dwThis);

typedef int (_stdcall *FUNC1)(int a, int b);

#pragma pack(push,1)

//先将目前位元組對齊值壓入編譯棧棧頂, 然後再将 n 設為目前值

typedef struct tagTHUNK

{

BYTE bMovEcx; //MOVE ECX Move this point to ECX

DWORD dwThis; // address of this pointer

BYTE bJmp; //jmp

DWORD dwRealProc; //proc offset Jump Offset

void Init(DWORD proc,void* pThis)

{

bMovEcx = 0xB9;

dwThis = (DWORD)pThis;

bJmp = 0xE9;

dwRealProc = DWORD((INT_PTR)proc - ((INT_PTR)this+sizeof(THUNK)));

//jmp跳轉的是目前指令位址的偏移,也就是成員函數位址與目前指令的位址偏移

FlushInstructionCache(GetCurrentProcess(),this,sizeof(THUNK));

// 因為修改了資料,是以調用FlushInstructionCache,重新整理緩存

}

}THUNK;

// BYTE bMovEcx; DWORD dwThis; 這兩句連起來就是把this指針儲存到了ECX的寄存器

// BYTE bJmp; DWORD dwRealProc;就是跳轉到成員函數的位址

#pragma pack(pop)

//将編譯棧棧頂的位元組對齊值彈出并設為目前值.

template<typename dst_type, typename src_type>

dst_type pointer_cast(src_type src)

{

return *static_cast<dst_type*>(static_cast<void*>(&src));

}

class Test

{

public:

int m_nFirst;

THUNK m_thunk;

  int m_nTest;

Test() : m_nTest(3),m_nFirst(4)

{}

void TestThunk()

{

m_thunk.Init(pointer_cast<int>(&Test::Test2),this);

FUNC1 f = (FUNC1)&m_thunk;

f(1,2);

cout << "Test::TestThunk()" << endl;

}

int Test2(int a, int b)

{

cout << a << " " << b << " " << m_nFirst << " " << m_nTest << " <<I am in Test2" << endl;

return 0;

}

};

int main(int argc, _TCHAR* argv[])

{

Test t;

t.TestThunk();

//system("pause");

return 0;

}

PS:可以看出上面的方法是将代碼寫入資料段,達到了強制跳轉的目的,在這個過程中一定要弄清楚函數調用規則和堆棧的平衡。

在指針轉化中使用了pointer_cast函數,也可以這樣進行:

template<class ToType,  class FromType>

void GetMemberFuncAddr_VC6(ToType& addr,FromType f)

{

    union

    {

        FromType _f;

        ToType   _t;

    }ut;

    ut._f = f;

    addr = ut._t;

}

使用的時候:

    DWORD dPtr;

    GetMemberFuncAddr_VC6(dPtr,Class::Function); //取成員函數位址.

    FUNCTYPE pFunPtr  = (FUNCTYPE) dPtr;//将函數位址轉化為普通函數的指針

因為在類的方法預設的調用規則是thiscall,是以上面在進行回調的過程中采用了在ecx中傳入this指針的方法,也就是this指針通過ecx寄存器進行傳遞。注意,在VC6中是沒有__thiscall關鍵字的,如果使用了編譯器會報錯。