C++11 成員函數作為回調函數的使用方式
C++11成員函數作為回調函數使用
std::bind()被廣泛地應用在新式的回調函數中。
C++11以前類的普通成員函數不能作為回調函數去註冊,因為將普通成員函數註冊給對方,但對方使用這個函數指針時,就會發生參數列表匹配的問題,因為少瞭隱含的this。
靜態成員函數不包含this指針,所以一般將靜態成員函數註冊給對方。
C++11推出std::bind()和std::function搭配,前者生成新的調用對象,參數個數可以小於綁定函數的參數個數,少的參數,按位占用。後者保存函數調用類型的函數對象,使用該對象進行設置參數即可。
示例1
先看一個例子來熱熱身,熟悉一下std::bind和std::function
#include <functional> //所需std::bind和std::function頭文件 #include <iostream> #include <map> using namespace std; // 使用std::bind時記得和::bind區別開,就怕作用於污染,誤用::bind //除法運算 class Division { public: int operator()(int i, int j) { return i / j; } }; //乘法運算 int Multiplication(int i, int j) { return i * j; } //減法運算 int Substraction(int i, int j) { return i - j; } //回調註冊函數 int CallbackReg(function<int(int, int)> &func, int i, int j) { return func(i, j); } //回調註冊函數1, int CallbackReq1(function<int(int)> &&func, int i) { return func(i); } int main() { // 此function接受函數調用類型為int(int, int)的調用對象 function<int(int, int)> func1 = [](int i, int j) { return i + j; }; //lambda function<int(int, int)> func2 = &Substraction; //函數指針 function<int(int, int)> func3 = Multiplication; //函數名 function<int(int, int)> func4 = Division(); //重載調用運算符的對象 //可將function類型存在容器中,來一次映射 map<int, function<int(int, int)>> mpFuncs; mpFuncs[1] = func1; mpFuncs[2] = func2; mpFuncs[3] = func3; mpFuncs[4] = func4; //這裡做著玩,映射一個數字和字符串 map<int, string> mpOprs{{1, " + "}, {2, " - "}, {3, " * "}, {4, " / "}}; // 便利map調用容器內函數對象們 for (auto& it : mpFuncs) { cout << "calculator :" << 20 << mpOprs[it.first] << 5 << " = " << CallbackReg(it.second, 20, 5) << endl; } //使用std::bind,產生一個新的調用對象bindFunc(int i), 200作為int Multiplication(int i, 200) //std::placeholders 有個N個占位符(vs此版為20個):_N,表示占用綁定函數的第n個位子 int pre = 300; auto bindFunc = std::bind(Multiplication, placeholders::_1, pre); cout << bindFunc(3) << endl; //bind最重要的一點在於參數綁定,如下例註冊回調,參數就從2個變成瞭1個 cout << CallbackReq1(std::bind(Multiplication, placeholders::_1, 200), 2) << endl; return 0; }
calculator :20 + 5 = 25
calculator :20 – 5 = 15
calculator :20 * 5 = 100
calculator :20 / 5 = 4
900
400
好,在瞭解瞭std::bind和std::function之後來看一個平時常遇到的C++式的回調函數註冊
示例2
#include <functional> #include <iostream> #include <string> #include <memory> using namespace std; using namespace std::placeholders; //占位符_N所在的命名空間 using CallBackFuncType = function<void(string const&)>; class Client { public: string name; CallBackFuncType serverFunc; Client() :name("Vergo"), serverFunc(nullptr) {} ~Client() {} void SetCallBack(const CallBackFuncType &func) { serverFunc = func; } void DoCallBack() { serverFunc(name); } }; class Server { public: Client *m_clt; Server() : m_clt(nullptr) { m_clt = new Client; } ~Server() { if (m_clt) delete m_clt; m_clt = nullptr; } //回調函數本數 void MyCallBackFunc(string const& str) { cout << "The name of client is " << str << endl; } //註冊回調,將this指針綁定到回調函數中 void RegCallBackFunc() { if (!m_clt) return; m_clt->SetCallBack(CallBackFuncType(std::bind(&Server::MyCallBackFunc, this, _1))); } //回調 void GiveMeCallBack() { if (!m_clt) return; m_clt->DoCallBack(); } }; int main() { Server testClass; testClass.RegCallBackFunc(); testClass.GiveMeCallBack(); return 0; }
The name of client is Vergo
類成員函數作為回調函數的方法及註意點
編程中遇到一個錯誤,提示為error C2597: illegal reference to non-static member
即因為一個類的靜態成員函數調用瞭類的非靜態成員變量,而報錯。
下面具體介紹一些相關知識點,以防下次再出錯。
類成員函數當回調函數的方法
方法一:回調函數為普通的全局函數,但在函數體內執行類的成員函數
在創建線程調用回調函數時,傳入類對象的指針(比如this指針)作為參數,並在回調函數中把void*強制轉換為類的指針(MyClass*),就能使用該指針調用類的成員函數。
這樣做的原理是把當前對象的指針當作參數先交給一個外部函數,再由外部函數調用類成員函數。以外部函數作為回調函數,但執行的是成員函數的功能,這樣相當於在中間作瞭一層轉換。
缺點:回調函數在類外,影響瞭封裝性。
方法二:回調函數為類內靜態成員函數,在其內部調用類的非靜態成員函數
此時需要一個指向類本身的、類的靜態成員變量指針(static MyClass* CurMy),用來存儲當前回調函數調用的對象,相當於法1中給回調函數傳入的指針參數。在回調函數中通過CurMy指針調用類的成員函數。
優點:
- 1、解決瞭法1的封裝性問題
- 2、沒有占用callback的參數,可以從外界傳遞參數進來
缺點:每個對象啟動子線程前一定要註意先讓CurMy正確的指向自身,否則將為其它對象開啟線程。
方法三:對成員函數進行強制轉換,使其作為回調函數
這個方法是原理是,MyClass::func最終會轉化成 void func(MyClass *this);即在原第一個參數前插入指向對象本身的this指針。可以利用這個特性寫一個非靜態類成員方法來直接作為線程回調函數。
typedef void* (*FUNC)(void*); FUNC callback = (FUNC)&MyClass::func;
對編譯器而言,void (MyClass::*FUNC1)()和void* (*FUNC)(void*)這兩種函數指針雖然看上去很不一樣,但他們的最終形式是相同的,因此就可以把成員函數指針強制轉換成普通函數的指針來當作回調函數。在建立線程時要把當前對象的指針this當作參數傳給回調函數(成員函數func),這樣才能知道線程是針對哪個對象建立的。
註意:此方法中FUNC函數的參數一定要是void*,這樣才能在編譯後把this指針轉變為MyClass *this。
優點:法3的封裝性比法2更好,因為不涉及多個對象共用一個靜態成員的問題,每個對象可以獨立地啟動自己的線程而不影響其它對象。
為什麼回調函數必須為靜態函數?
普通的C++成員函數都隱含瞭一個“this”指針參數,當在類的非靜態成員函數中訪問類的非靜態成員時,C++編譯器通過傳遞一個指向對象本身的指針給其成員函數,從而能夠訪問類的數據成員。也就是說,即使你沒有寫上this指針,編譯器在編譯的時候自動加上this的,它作為非靜態成員函數的隱含形參,對各成員的訪問均通過this進行。
正是由於this指針的作用,使得將一個CALLBACK型的成員函數作為回調函數時就會因為隱含的this指針使得函數參數個數不匹配,從而導致回調函數匹配失敗。所以為瞭實現回調,類中的成員函數必須舍棄掉隱藏的this指針參數。因此,類中的回調函數必須為靜態函數,加上static關鍵字。
類的靜態成員函數如何訪問非靜態成員?
靜態成員不屬於某個具體的對象,而是被所有對象所共享。即靜態成員屬於整個類,不屬於具體某個對象;非靜態成員屬於具體某個對象。因而靜態成員函數隻能訪問類的靜態成員,不能訪問類中非靜態成員。
那麼,如何讓靜態函數訪問類的非靜態成員?
方法是:對於靜態成員函數,我們顯示的為其傳遞一個對象的首地址(該類的指針)。一般在這個靜態成員函數的形參列表中加入一個 void* 類型的參數,來保存對象的首地址。並在該函數內部對該參數進行類型轉換,通過類型轉換後的參數來調用非靜態成員。
或者用一個類的全局指針數組,保存每一個創建出來的類的this指針,用全局指針去調用。
以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。
推薦閱讀:
- C++11標準庫bind函數應用教程
- C++ std::bind用法詳解
- 簡單聊聊C++中回調函數的實現
- C++超詳細梳理lambda和function的使用方法
- C++類重載函數的function和bind使用示例