C++11绑定器bind及function机制
🚀 優質資源分享 🚀
| 🧡 Python實戰微信訂餐小程序 🧡 | 進階級 | 本課程是python flask+微信小程序的完美結合,從項目搭建到騰訊云部署上線,打造一個全棧訂餐系統。 |
| 💛Python量化交易實戰💛 | 入門級 | 手把手帶你打造一個易擴展、更安全、效率更高的量化交易系統 |
前言
之前在學muduo網絡庫時,看到陳碩以基于對象編程的方式,大量使用boost庫中的bind和function機制,如今,這些概念都已引入至C++11,包含在頭文件中。
本篇文章主要梳理C++綁定器相關的內容以及C++11中引入的function機制,其中綁定器主要有三種:bind1st、bind2nd、bind(C++11)。學完本篇內容,將對C++綁定器及function機制等的底層實現有深刻理解,那么我們開始說吧。
函數對象
首先說說函數對象,之所以說函數對象,是因為綁定器、function都涉及到該部分概念。函數對象實際上是類調用operator()()小括號運算符重載,實現像在“調用函數”一樣的效果,因此還有個別名叫“仿函數”。函數對象示例代碼如下:
class Print { public:void operator()(string &s) { cout << s << endl; } };int main() {string s = "hello world!";Print print; //定義了一個函數對象printprint(s);return 0; }上面代碼print(s);語句,看似像函數調用,其實是類對象print調用其小括號運算符重載print.operator(string &s)。print就是一個函數對象,至此對函數對象就有了基本的認識。
剖析綁定器bind1st、bind2nd
了解了函數對象,接下來我們說說綁定器,為什么需要綁定器?在使用STL時經常會遇到STL算法中需要傳遞某元函數對象,比如在寫sort時,第三個參數決定了我們的排序規則,用來接收一個“比較器”函數對象,該函數對象是一個二元的匿名函數對象,形如greator()或者less()。二元函數對象的意思是,這個函數對象的小括號運算符重載函數接收兩個參數,那么幾元就表示接收幾個參數。下面是庫中自帶的greater和less模板類的源碼實現,可以看到是對小括號運算符重載的實現,sort第三個參數接收該模板類的二元匿名函數對象。
template<typename \_Tp>struct greater : public binary\_function<\_Tp, \_Tp, bool>{_GLIBCXX14_CONSTEXPRbooloperator()(const _Tp& __x, const _Tp& __y) const{ return __x > __y; }};template<typename \_Tp>struct less : public binary\_function<\_Tp, \_Tp, bool>{_GLIBCXX14_CONSTEXPRbooloperator()(const _Tp& __x, const _Tp& __y) const{ return __x < __y; }};再回到剛才的問題,那為什么需綁定器?由于STL接口的限制,有時我們拿到的函數對象和特定STL算法中要接收的函數對象在參數上并不匹配,意思就是需要傳遞一個一元函數對象,你有一個二元函數對象,那可以通過綁定器提前綁定二元函數對象的其中一個參數,使得最終返回的是一個一元函數對象,那么從二元函數對象到一元函數對象的轉換過程,就需要綁定器去實現。
如STL中的泛型算法find_if,可用來查找可變長數組vector中符合某個條件的值(這個條件比如是要大于50,要小于30,要等于25等等)。其第三個參數需要傳遞一個一元函數對象,假如現在要找到第一個小于70的數,可將綁定器與二元函數對象結合,轉換為一元函數對象后傳遞給find_if。
我們知道系統自帶的greater()和less()模板類對象是二元匿名函數對象,所以需要通過綁定器將其轉換為一元函數對象,可以通過bind1st和bind2nd去綁定,顧名思義,前者對二元函數對象的第一個參數進行綁定,后者對二元函數對象的第二個參數進行綁定,兩個綁定器均返回一元函數對象,用法如下:
sort(vec.begin(), vec.end(), greater<int>()); //從大到小對vector進行排序 find\_if(vec.begin(), vec.end(), bind1st(greater<int>(), 70)); find\_if(vec.begin(), vec.end(), bind2nd(less<int>(), 70));兩個綁定器分別提前綁定了一個參數,使得二元函數對象+綁定器轉換為一元函數對象:
operator()(const T &val) greater a > b ====> bind1st(greater<int>(), 70) ====> 70 > b less a < b ====> bind2nd(less<int>(), 70) ====> a < 70下面給出bind1st綁定過程圖,二元函數對象綁定了第一個數為70,變為一元函數對象,傳遞給find_if泛型算法,此時find_if所實現的功能就是:找出有序降序數組中第一個小于70的數,所以find_if返回指向65元素的迭代器:
file:///Users/guochen/Notes/docs/media/16656563650484/16657214749366.jpg
以上就是綁定器的概念。因此需要綁定器的原因就很明顯了,綁定器可以返回一個轉換后的某元函數對象,用于匹配泛型算法。
根據上面的理解,接下來實現一下bind1st,代碼實現如下:
/*可以看到 自己實現的綁定器本質上也是個函數對象 調用operator()進行綁定*/ template<typename Compare, typename T> class \_mybind1st { public:\_mybind1st(Compare comp, T first) : \_comp(comp), \_val(first) {}bool operator()(const T &second) {return \_comp(_val, second);} private:Compare _comp;T _val; };/*實現bind1st 函數模板*/ //直接使用函數模板,好處是可以進行類型推演 template<typename Compare, typename T> _mybind1st<Compare, T> mybind1st(Compare comp, const T &val) { //綁定器返回值\_mybind1st為一元函數對象return _mybind1st<Compare, T>(comp, val); }上述代碼中mybind1st綁定器第一個參數Compare comp是要綁定的二元函數對象,第二個參數val是在原有函數對象上綁定的值,最后綁定器調用_mybind1st模板函數對象的小括號運算符重載并返回該一元匿名函數對象,可以看到_mybind1st小括號運算符重載中已將綁定器mybind1st第二個參數val傳遞給了原本的二元函數對象Compare comp,因此原本綁定器接收的二元函數對象只需要處理第二個參數。所以綁定器返回的函數對象_mybind1st其實是在原本的函數對象上套了一層參數的新的函數對象,閱讀上面的代碼實現,就可更深刻的理解bind1st的底層原理。
與此同時,不難寫出bind2nd的實現,顧名思義該綁定器是對第二個參數進行綁定,不過多贅述,貼出實現代碼:
template<typename Compare, typename T> class \_mybind2nd { public:\_mybind2nd(Compare comp, T second) : \_comp(comp), \_val(second) {}bool operator()(const T &first) {return \_comp(first, _val);} private:Compare _comp;T _val; };template<typename Compare, typename T> _mybind2nd<Compare, T> mybind2nd(Compare comp, const T &val) {return _mybind2nd<Compare, T>(comp, val); }根據上文,我們清楚了解到泛型算法find_if第三個參數接收一元函數對象,且該泛型算法功能是尋找第一個符合某條件的元素,我們對其補充實現,代碼貼出:
/** * 自己實現了find\_if后發現其實綁定器返回的就是綁定后的函數對象* 使用綁定器的目的:就是將原本某元的函數對象轉化為另一個元的函數對象* 說白了,綁定器還是對函數對象的一個應用**/ template<typename Iterator, typename Compare> Iterator my\_find\_if(Iterator first, Iterator last, Compare comp) {for(; first != last; ++first) {if(comp(*first)) { //調用comp的小括號運算符重載 一元函數對象 comp.operator()(*first)return first;}}return last; }此時要尋找vector中第一個小于70的數,就可以這樣寫:
auto it = my\_find\_if(vec.begin(), vec.end(), mybind1st(greater<int>(), 70)); cout << *it << endl; //打印vec中第一個小于70的數值以上,圍繞bind1st、bind2nd以及函數對象等,展開討論了綁定器bind1st、bind2nd的實現原理,但是同時我們也發現其缺點,就是只能對二元函數對象進行綁定轉換,讓其轉換為一元函數對象,那如果遇到很多元的函數對象,我們還得一個一個自己去實現嗎?所以將boost庫的boost::bind引入到了C++11標準庫中,接下來我們介紹C++11的綁定器std::bind,它是對上述兩種綁定器的泛化。支持任意函數對象(其實標準庫中最多支持29元函數對象,不過這也足夠使用了)。
補充:上面都是以函數對象為例,作為綁定器第一個參數傳遞,其實第一個參數可以是函數對象、成員函數、也可以是普通函數。
總結:綁定器本身是函數模板,綁定器第一個參數可能是普通函數、成員函數或函數對象等,返回的一定是函數對象。還有就是這兩個綁定器在C++17中已移除,因此僅用于學習和理解綁定器,也方便我們對C++11引入的bind的學習。至于當前這兩個綁定器如何實現對類成員函數的綁定等等我們也沒必要去尋找答案了(我一開始也在努力尋找如何使用這兩個綁定器去綁定類成員函數,但是發現bind可以很輕松地做到,當然如果大家知道怎么使用bind1st和bind2nd綁定類成員函數,也可以評論告知我,感謝~)。
C++11 bind通用綁定器(函數適配器)
我們可將bind函數看作是一個通用的函數適配器,它接受一個可調用函數對象,生成一個新的可調用函數對象來“適應”原對象的參數列表。bind相比于bind1st和bind2nd,實現了“動態生成新的函數”的功能。簡言之,可通過bind函數修改原函數并生成一個可以被調用的對象,類似于函數的重載,但是我們又不需要去重新寫一個函數,用bind函數就可以實現。相信在上面講bind1st和bind2nd時,大家對這些關于綁定器(函數適配器)的概念已經有所認知,我們直接看看如何用的吧。
綁定一個普通函數和函數指針
#include #include using namespace std; using namespace placeholders; int fun(int a, int b, int c, int d, int e) {return a + b - c + d - e; } int main() {int x = 1, y = 2, z = 3;auto g = bind(fun, x, y, _2, z, _1); //第一個參數&可省略 但最好寫成&funcout << g(11, 22) << endl; // fun(1, 2, 22, 3, 11) => 1+2-22+3-11// cout << bind(fun, x, y, \_2, z, \_1)(11, 22) << endl; //等價 }g是有兩個參數的二元函數對象,其兩個參數分別用占位符placeholders::_2和placeholders::_1表示,_2代表二元函數對象的第二個參數22,_1代表二元函數對象的第一個參數11。這個新的可調用對象將它自己的參數作為第三個和第五個傳遞給fun,fun函數的第一個、第二個第四個參數分別被綁定到給定的值x、y、z上。
綁定一個類的靜態成員函數與綁定全局函數沒有任何區別,這里不做說明,可參考文章:[ 🔗 bind綁定器使用方法 ],該文章中bind詳細用法中描述了對靜態成員方法的使用。
綁定一個類成員函數
綁定器綁定一個成員函數時,我們知道非靜態成員函數第一個參數隱藏了一個this指針對象,所以綁定時綁定器第二個參數傳遞匿名類對象本身。bind和之前的bind1st、bind2nd一樣,最終返回的一定是函數對象,下面的代碼將一個五元函數綁定后,返回了一個三元函數對象,效果等同于調用f.operator()(10, 6, 7)。
#include #include using namespace std; using namespace placeholders; class Test { public:int func(int a, int b, int c, int d, int e) { return a + b - c + d - e; } };int main() {auto f = bind(&Test::func, Test(), _1, 12, _3, 5, _2);cout << f(10, 6, 7) << endl; //輸出:10+12-7+5-6 = 14cout << f.operator()(10, 6, 7) << endl; }作為類成員函數,需要注意的一點是,如果是非靜態的成員函數,它會存在一個默認的this指針,靜態的成員函數則不存在this指針,所以在將其作為bind函數的參數時,需要注意使用this指針作為其中一個參數,當使用靜態成員函數作為參數時,其用法和全局函數類似,當參數為類內非靜態成員函數時,第一個參數必須使用&符號。
注:成為成員函數時,第一個參數之所以必須使用&符號,這部分原因可參考:[ 🔗 C++中普通函數指針與類成員函數指針的異同 ],文章中有說明具體原因。
以上就是C++11 bind的使用方法,衍生于bind1st、bind2nd,支持更多的參數綁定,關于bind函數更多的使用方法,也可參考C++Boost的說明文檔:[ 🔗 bind原理圖釋 ],該文章中的圖片方便我們對綁定過程的理解。
C++11 function機制
C++11的function機制是C語言中函數指針的衍生,用來實現回調功能,我們上面的綁定器通常都是以語句執行為單位,當出了某個語句的執行后,綁定器返回的這個函數對象也就隨之消失,因此需要有回調功能的function去長期保留綁定器返回的函數對象,以便在需要的時候隨時通過function機制調用即可。那有人會問,既然有函數指針,為什么還要再整出來一個function機制?這不是多此一舉嗎?答案肯定是:很有必要,因為function能做到的,函數指針未必能做到,接下來容我花點篇幅去說明為什么C++中有函數指針還需要std::function。
為什么C++中有函數指針還需要std::function?
C/C++中可以使用指針指向一段代碼,這個指針就叫函數指針,假設有這樣一段代碼:
#include int func(int a) { return a + 1; }int main() {int (*f)(int) = func;printf("%p\n", f);return 0; }我們定義了一個函數func,然后使用指針變量f指向該函數,然后打印出變量f指向的地址,代碼很簡單,然后我們編譯一下,看下編譯后生成的指令,我們重點關注func函數:
int func(int a) {4005b6: 55 push %rbp4005b7: 48 89 e5 mov %rsp,%rbp4005ba: 89 7d fc mov %edi,-0x4(%rbp)return a + 1;4005bd: 8b 45 fc mov -0x4(%rbp),%eax4005c0: 83 c0 01 add $0x1,%eax }4005c3: 5d pop %rbp4005c4: c3 retq可以看到,編譯好后的函數func位于地址0x4005b6這個地址,讓我們記住這個地址。然后運行一下編譯后生成的程序,想一想這段代碼會輸出什么呢?顯然應該是func函數的在內存中的地址!
[root@localhost 07]# ./a.out 0x4005b6沒有猜錯吧,實際上函數指針本質也是一個指針,只不過這個指針指向的不是內存中的一段數據而是內存中的一段代碼,就像這樣:
看到了吧,我們常說的指針一般都是指向內存中的一段數據,而函數指針指向了內存中的一段代碼,在這個示例中指向了內存地址0x4005b6,在這個地址中保存了函數func的機器指令。
現在你應該明白函數指針了,細心的同學可能會有一個疑問,為什么編譯器在生成可執行文件時就知道函數func存放在內存地址0x4005b6上呢?這不應該是程序被加載到內存后開始運行時才能確定的嗎?
函數指針的作用是可以把一段代碼當做一個變量傳來傳去,主要的用途之一就是回調函數。關于回調函數其實是在A模塊定義,在B模塊被調用,就像這樣:
然而有時我們會有這樣的場景,我們依然需要在模塊A定義函數,同時函數A的運行需要依賴B模塊產生的數據,然后將模塊A定義的函數和模塊B產生的數據一并傳遞給C模塊來調用,就像這樣:
此時,單純的函數指針已經不夠用了,因為函數指針只是單純的指向了內存中的一段代碼,我們不但需要將內存中的一段代碼同時也需要將內存中的一塊數據傳遞給模塊C,此時你可以定義一個結構體,將代碼和數據打包起來,就像這樣:
typedef void (*func)(int);struct functor {func f;int arg; };我們將這個結構體命名為functor,注意看,這個結構中有兩部分:
- 一個指向代碼的指針變量
- 一個保存數據的變量
這樣,我們在A模塊為指針變量賦值,在B模塊為保存數據的變量賦值,然后將此結構體傳遞給模塊C,模塊C中可以這樣使用:
void run(struct functor func) {func->f(func->arg); }即,functor既包含了一段代碼也包含了這段代碼使用的數據,這里的數據也被稱為context,即上下文,或者environment,即環境,不管怎么稱呼,其實就是函數運行依賴的數據:
而這也正是C++中std::function的目的所在。
單純的函數指針并沒有捕捉上下文的能力,這里的上下文就是指代碼依賴的數據,你不得不自己動手構造出一個結構體用來存儲代碼依賴的上下文。在C++中你沒有辦法單純的利用函數指針指向對象的成員函數,就是因為函數指針沒有辦法捕捉this(指向對象的指針)這個上下文。
??注:std::function的作用本質上和我們剛才定義的結構體區別不大。
利用std::function你不但可以保存一段代碼,同時也可以保存必要的上下文,然后在合適的地方基于上下文調用這段代碼。
根據上文的介紹,我們也知道std::function相比函數指針的優勢所在,要去理解std::function,只需要理解上面提到的結構體即可。接下來我們來談談std::function的用法以及一步一步實現一個簡單的std::function,剖析其原理。
function的基本用法
接下來直接展示function的直觀用法,我們可以把function想象為一個模板類,調用該模板類的operator()()小括號運算符重載,執行封裝的函數指針,關于std::function具體實現細節,后續再繼續說明,函數指針可用于回調功能,函數對象也可用于回調功能,lambda表達式也可用于回調功能,甚至bind綁定適配后的成員函數也可用于回調功能,那么在不確定的情況下,通過function機制這樣的泛型機制統一表示,就會很方便。
普通函數:
void hello() {cout << "hello world!" << endl;} void hello\_str(string str) {cout << str << endl;} int main() {function<void()> func1 = &hello;// function func1(&hello); // 兩種調用方法均可func1(); //調用func1.operator()() ==> void hello()function<void(string)> func2 = &hello_str;func2("hello world"); //調用func2.operator()(string) ==> void hello\_str(string)return 0; }模板函數:
template<typename T> T sum(T a, T b) { return a + b; } int main() {function<int(int, int)> func1 = sum<int>;//調用func1.operator()(int, int) ==> int sum(int, int);cout << func1(3, 5) << endl; //輸出8return 0; }lambda表達式:
int main() {function<int(int, int)> func1 = [](int a, int b)->int { return a + b; };cout << func1(3, 5) << endl; //打印8 調用func1.operator()(int, int) ==> 調用lambda表達式返回求和結果return 0; }函數對象:
class PrintAdd1 { public:void operator()(int left, int right) {cout << "sum : " << left + right << endl;} }; int main() {function<void(int, int)> func1 = PrintAdd1(); //調用默認無參構造函數創建匿名類對象給func1func1(3, 5); //func1.operator()(int, int) ==> 調用void PrintAdd1.operator(int, int)return 0; }模板函數對象:
template<typename T> class PrintAdd2 { public:void operator()(T left, T right) {cout << "sum : " << left + right << endl;} }; int main() {function<void(int, int)> func1 = PrintAdd2<int>(); //調用默認無參構造函數創建匿名模板類對象給func1func1(3, 5); //func1.operator()(int, int) ==> 調用void PrintAdd2.operator()(int, int)return 0; }類靜態成員函數:
class StaticClass1 { public:static void hello\_static(string s) {cout << s << endl;} }; int main() {function<void(string)> func1 = &StaticClass1::hello_static;func1("hello world"); //func1.operator()(string) ==> 調用void hello\_static(string)return 0; }模板類靜態成員函數:
template<typename T> class StaticClass2 { public:static void hello\_static(T out) {cout << out << endl;} }; int main() {function<void(string)> func1 = &StaticClass2<string>::hello_static;func1("static.. hello world"); //func1.operator()(string) ==> 調用void StaticClass2::hello\_static(string)return 0; }普通類成員函數:
class Test { public:void hello(string str) {cout << str << endl;} }; int main() {// function func = &Test::hello;// func(&Test(), "call Test::hello"); //這種第一個參數傳遞匿名對象的方法在GCC8.4下不可行 在vs2017下可行 不建議使用匿名對象Test test; //定義對象function<void(Test *, string)> func1 = &Test::hello;func1(&test, "call Test::hello"); //func1.operator(Test *, string) ==> 調用void Test::hello(string)return 0; }模板類成員函數:
template<typename T> class Test2 { public:void hello(T str) {cout << str << endl;} }; int main() {Test2<string> test2;function<void(Test2<string> *, string)> func1 = &Test2<string>::hello;func1(&test2, "call template Test::hello"); //func1.operator(Test2 *, string) ==> 調用void Test2::hello(string)return 0; }function底層原理剖析
對function用法有基本了解后,為了剖析function底層原理,我們還需知道模板的「特例化」以及「可變參數模板』,這里不再說明,可參考我以下兩篇博文,已經對模板特化和可變參數模板進行了解釋:
- [ 🔗 模板全特化與偏特化的概念 ]
- [ 🔗 泛化之美 —— C++11 可變參數模板的妙用 ]
function是C++11特有的一種比函數指針更靈活的機制,現在如果我們要接收一個hello函數,形如:
void hello(string str) { cout << str << endl; }該如何實現呢?function利用了函數對象的手段,結合函數指針去調用小括號運算符重載實現,因此理所應當的實現是這樣的,內部有一個函數指針_pfunc,并且該函數指針在operator()小括號運算符重載重被調用:
//前向聲明模板類 template<typename T> class myfunction {};//單個類型參數模板完全特例化 template<typename R, typename A1> class myfunction<R(A1)> { public:using PFUNC = R (*)(A1); public:myfunction(PFUNC pfunc) : \_pfunc(pfunc) {}R operator()(A1 arg) {return \_pfunc(arg);} private:PFUNC _pfunc; };當function對象接收hello函數時,R作為返回值會被推導為void,A1作為單一參數類型被推導為string類型:
myfunction<void(string)> f1 = &hello; f1("hello world"); //打印"hello world"那現在如果我們要接收兩個參數的sum求和函數呢?
int sum(int a, int b) { return a + b; }其實理解了function的原理后,這個時候要接收一個雙參數sum求和函數,可以再去特化一個適合sum的類,其中R推導為int,A1為sum的第一個參數類型int,A2為sum的第二個類型int:
#include using namespace std;int sum(int a, int b) { return a + b; }template<typename T> class myfunction {};//兩個類型參數模板完全特例化 template<typename R, typename A1, typename A2> class myfunction<R(A1, A2)> { public:using PFUNC = R (*)(A1, A2); public:myfunction(PFUNC pfunc) : \_pfunc(pfunc) {}R operator()(A1 arg1, A2 arg2) {return \_pfunc(arg1, arg2);} private:PFUNC _pfunc; };int main() {myfunction<int(int, int)> f2 = ∑// myfunction f2(&sum);cout << f2(3, 4) << endl; // 輸出5return 0; }以上就是function去接收單參數和雙參數時的實現,但是這有個很致命的缺點,如果要接收三個參數、四個參數、十個參數、一百個參數、一千個參數呢?(當然這不太可能,就是單純去考慮這種情況),那是不是還得對不同參數都進行一次實現?那模板的意義何在?如何消除代碼冗余就是問題所在,我們需要用到可變模板參數,我的這篇文章已經說明什么是可變參數模板,如何去使用它:[ 🔗 泛化之美 —— C++11 可變參數模板的妙用 ]
所以通過可變模板參數,我們去實現一個可變參數的function,該function可以接收任意數量參數的函數、函數對象、lambda表達式、bind適配后的成員函數和普通函數等等:
template<typename T> class myfunction {};/*那如果類型參數多了,我們還要一個一個定義嗎??其實可以采用變長參數模板實現*/ // 可變參數模板完全特例化 template<typename R, typename... A> class myfunction<R(A...)> { public:using PFUNC = R (*)(A...); public:myfunction(PFUNC pfunc) : \_pfunc(pfunc) {}R operator()(A... arg) {return \_pfunc(arg...);} private:PFUNC _pfunc; };根據以上實現,我們對function的底層原理有了深刻的認識,能夠自己實現一個接收任意參數的function模板類。雖然以上實現和標準庫還差得很遠,但是起到了學習、理解的作用吧。
總結
本文通過C++11之前的bind1st和bind2nd引入,介紹了C++11的bind的使用,理清了它的作用,何謂“綁定”?然后介紹了function的使用方法,并對其進行了邏輯上的實現,以上這些知識點都挺難的,涉及函數對象、模板特化、可變參數模板。但是也算是一次系統性總結吧,感覺意義蠻大的。C++11的路還很遠,以后有機會再繼續深耕吧。
順便記錄一下,我還在為自己的秋招迷茫中,如果有好消息,我在回來還愿!
總結
以上是生活随笔為你收集整理的C++11绑定器bind及function机制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 文字识别软件测试初学者,【只要10分钟
- 下一篇: 国外设计博客小组收集