轉載來自:https://subingwen.cn/cpp/bind/
1. 可調用對象
在 C++ 中存在 “可調用對象” 這麼一個概念。準確來說,可調用對象有如下幾種定義:
是一個函數指針
int print(int a, double b) { cout << a << b << endl; return 0; }
// 定義函數指針
int (*func)(int, double) = &print;
是一個具有operator()成員函數的類對象(仿函數)
#include <iostream> #include <string> #include <vector> using namespace std; struct Test { // ()操作符重載 void operator()(string msg) { cout << "msg: " << msg << endl; } }; int main(void) { Test t; t("我是要成爲海賊王的男人!!!"); // 仿函數 return 0; }
是一個可被轉換爲函數指針的類對象
#include <iostream> #include <string> #include <vector> using namespace std; using func_ptr = void(*)(int, string); struct Test { static void print(int a, string b) { cout << "name: " << b << ", age: " << a << endl; } // 將類對象轉換爲函數指針 operator func_ptr() { return print; } }; int main(void) { Test t; // 對象轉換爲函數指針, 並調用 t(19, "Monkey D. Luffy"); return 0; }
是一個類成員函數指針或者類成員指針
#include <iostream> #include <string> #include <vector> using namespace std; struct Test { void print(int a, string b) { cout << "name: " << b << ", age: " << a << endl; } int m_num; }; int main(void) { // 定義類成員函數指針指向類成員函數 void (Test:: * func_ptr)(int, string) = &Test::print; // 類成員指針指向類成員變量 int Test::* obj_ptr = &Test::m_num; Test t; // 通過類成員函數指針調用類成員函數 (t.*func_ptr)(19, "Monkey D. Luffy"); // 通過類成員指針初始化類成員變量 t.*obj_ptr = 1; cout << "number is: " << t.m_num << endl; return 0; }
在上面的例子中滿足條件的這些可調用對象對應的類型被統稱爲可調用類型。C++ 中的可調用類型雖然具有比較統一的操作形式,但定義方式五花八門,這樣在我們試圖使用統一的方式保存,或者傳遞一個可調用對象時會十分繁瑣。現在,C++11通過提供std::function 和 std::bind統一了可調用對象的各種操作。
2. 可調用對象包裝器
std::function是可調用對象的包裝器。它是一個類模板,可以容納除了類成員(函數)指針之外的所有可調用對象。通過指定它的模板參數,它可以用統一的方式處理函數、函數對象、函數指針,並允許保存和延遲執行它們。
2.1 基本用法
std::function 必須要包含一個叫做 functional 的頭文件,可調用對象包裝器使用語法如下:
#include <functional>
std::function<返回值類型(參數類型列表)> diy_name = 可調用對象;
下面的實例代碼中演示了可調用對象包裝器的基本使用方法:
#include <iostream> #include <functional> using namespace std; int add(int a, int b) { cout << a << " + " << b << " = " << a + b << endl; return a + b; } class T1 { public: static int sub(int a, int b) { cout << a << " - " << b << " = " << a - b << endl; return a - b; } }; class T2 { public: int operator()(int a, int b) { cout << a << " * " << b << " = " << a * b << endl; return a * b; } }; int main(void) { // 綁定一個普通函數 function<int(int, int)> f1 = add; // 綁定以靜態類成員函數 function<int(int, int)> f2 = T1::sub; // 綁定一個仿函數 T2 t; function<int(int, int)> f3 = t; // 函數調用 f1(9, 3); f2(9, 3); f3(9, 3); return 0; }
輸入結果如下:
9 + 3 = 12 9 - 3 = 6 9 * 3 = 27
通過測試代碼可以得到結論:std::function 可以將可調用對象進行包裝,得到一個統一的格式,包裝完成得到的對象相當於一個函數指針,和函數指針的使用方式相同,通過包裝器對象就可以完成對包裝的函數的調用了。
2.2 作爲回調函數使用
因爲回調函數本身就是通過函數指針實現的,使用對象包裝器可以取代函數指針的作用,來看一下下面的例子:
#include <iostream> #include <functional> using namespace std; class A { public: // 構造函數參數是一個包裝器對象 A(const function<void()>& f) : callback(f) { } void notify() { callback(); // 調用通過構造函數得到的函數指針 } private: function<void()> callback; }; class B { public: void operator()() { cout << "我是要成爲海賊王的男人!!!" << endl; } }; int main(void) { B b; A a(b); // 仿函數通過包裝器對象進行包裝 a.notify(); return 0; }
通過上面的例子可以看出,使用對象包裝器 std::function 可以非常方便的將仿函數轉換爲一個函數指針,通過進行函數指針的傳遞,在其他函數的合適的位置就可以調用這個包裝好的仿函數了。
另外,使用 std::function 作爲函數的傳入參數,可以將定義方式不相同的可調用對象進行統一的傳遞,這樣大大增加了程序的靈活性。
3. 綁定器
std::bind用來將可調用對象與其參數一起進行綁定。綁定後的結果可以使用std::function進行保存,並延遲調用到任何我們需要的時候。通俗來講,它主要有兩大作用:
將可調用對象與其參數一起綁定成一個仿函數。
將多元(參數個數爲n,n>1)可調用對象轉換爲一元或者(n-1)元可調用對象,即只綁定部分參數。
綁定器函數使用語法格式如下:
// 綁定非類成員函數/變量 auto f = std::bind(可調用對象地址, 綁定的參數/佔位符); // 綁定類成員函/變量 auto f = std::bind(類函數/成員地址, 類實例對象地址, 綁定的參數/佔位符); 下面來看一個關於綁定器的實際使用的例子:
#include <iostream> #include <functional> using namespace std; void callFunc(int x, const function<void(int)>& f) { if (x % 2 == 0) { f(x); } } void output(int x) { cout << x << " "; } void output_add(int x) { cout << x + 10 << " "; } int main(void) { // 使用綁定器綁定可調用對象和參數 auto f1 = bind(output, placeholders::_1); for (int i = 0; i < 10; ++i) { callFunc(i, f1); } cout << endl; auto f2 = bind(output_add, placeholders::_1); for (int i = 0; i < 10; ++i) { callFunc(i, f2); } cout << endl; return 0; }
測試代碼輸出的結果:
0 2 4 6 8 10 12 14 16 18
在上面的程序中,使用了 std::bind 綁定器,在函數外部通過綁定不同的函數,控制了最後執行的結果。std::bind綁定器返回的是一個仿函數類型,得到的返回值可以直接賦值給一個std::function,在使用的時候我們並不需要關心綁定器的返回值類型,使用auto進行自動類型推導就可以了。
placeholders::_1 是一個佔位符,代表這個位置將在函數調用時被傳入的第一個參數所替代。同樣還有其他的佔位符 placeholders::_2、placeholders::_3、placeholders::_4、placeholders::_5 等……
有了佔位符的概念之後,使得 std::bind 的使用變得非常靈活:
#include <iostream> #include <functional> using namespace std; void output(int x, int y) { cout << x << " " << y << endl; } int main(void) { // 使用綁定器綁定可調用對象和參數, 並調用得到的仿函數 bind(output, 1, 2)(); bind(output, placeholders::_1, 2)(10); bind(output, 2, placeholders::_1)(10); // error, 調用時沒有第二個參數 // bind(output, 2, placeholders::_2)(10); // 調用時第一個參數10被吞掉了,沒有被使用 bind(output, 2, placeholders::_2)(10, 20); bind(output, placeholders::_1, placeholders::_2)(10, 20); bind(output, placeholders::_2, placeholders::_1)(10, 20); return 0; }
示例代碼執行的結果:
1 2 // bind(output, 1, 2)(); 10 2 // bind(output, placeholders::_1, 2)(10); 2 10 // bind(output, 2, placeholders::_1)(10); 2 20 // bind(output, 2, placeholders::_2)(10, 20); 10 20 // bind(output, placeholders::_1, placeholders::_2)(10, 20); 20 10 // bind(output, placeholders::_2, placeholders::_1)(10, 20);
通過測試可以看到,std::bind 可以直接綁定函數的所有參數,也可以僅綁定部分參數。在綁定部分參數的時候,通過使用 std::placeholders 來決定空位參數將會屬於調用發生時的第幾個參數。
可調用對象包裝器 std::function 是不能實現對類成員函數指針或者類成員指針的包裝的,但是通過綁定器 std::bind 的配合之後,就可以完美的解決這個問題了,再來看一個例子,然後再解釋裏邊的細節:
#include <iostream> #include <functional> using namespace std; class Test { public: void output(int x, int y) { cout << "x: " << x << ", y: " << y << endl; } int m_number = 100; }; int main(void) { Test t; // 綁定類成員函數 function<void(int, int)> f1 = bind(&Test::output, &t, placeholders::_1, placeholders::_2); // 綁定類成員變量(公共) function<int& (void)> f2 = bind(&Test::m_number, &t); // 調用 f1(520, 1314); f2() = 2333; cout << "t.m_number: " << t.m_number << endl; return 0; }
示例代碼輸出的結果:
x: 520, y: 1314 t.m_number: 2333
在用綁定器綁定類成員函數或者成員變量的時候需要將它們所屬的實例對象一併傳遞到綁定器函數內部。f1的類型是function<void(int, int)>,通過使用std::bind將Test的成員函數output的地址和對象t綁定,並轉化爲一個仿函數並存儲到對象f1中。
使用綁定器綁定的類成員變量m_number得到的仿函數被存儲到了類型爲function<int&(void)>的包裝器對象f2中,並且可以在需要的時候修改這個成員。其中int是綁定的類成員的類型,並且允許修改綁定的變量,因此需要指定爲變量的引用,由於沒有參數因此參數列表指定爲void。
示例程序中是使用 function 包裝器保存了 bind 返回的仿函數,如果不知道包裝器的模板類型如何指定,可以直接使用 auto 進行類型的自動推導,這樣使用起來會更容易一些。