在設計回調函數的時候,無可避免地會接觸到可回調對象。在C++11中,提供了std::function
和std::bind
兩個方法來對可回調對象進行統一和封裝。
本文實例源碼github地址:https://github.com/yngzMiao/yngzmiao-blogs/tree/master/2019Q4/20191223。
可調用對象
C++中有如下幾種可調用對象:函數、函數指針、lambda表達式、bind對象、函數對象。其中,lambda表達式和bind對象是C++11標準中提出的(bind機制並不是新標準中首次提出,而是對舊版本中bind1st和bind2st的合併)。個人認爲五種可調用對象中,函數和函數指針本質相同,而lambda表達式、bind對象及函數對象則異曲同工。
函數
這裏的函數指的是普通函數,沒什麼可拓展的。
函數指針
插播一下函數指針和函數類型的區別:
- 函數指針指向的是函數而非對象。和其他指針類型一樣,函數指針指向某種特定類型;
- 函數類型由它的返回值和參數類型決定,與函數名無關。
例如:
bool fun(int a, int b)
上述函數的函數類型是:bool(int, int)
上述函數的函數指針pf是:bool (*pf)(int, int)
一般對於函數來說,函數名即爲函數指針:
# include <iostream>
int fun(int x, int y) { //被調用的函數
std::cout << x + y << std::endl;
return x + y;
}
int fun1(int (*fp)(int, int), int x, int y) { //形參爲函數指針
return fp(x, y);
}
typedef int (*Ftype)(int, int); //定義一個函數指針類型Ftype
int fun2(Ftype fp, int x, int y) {
return fp(x, y);
}
int main(){
fun1(fun, 100, 100); //函數fun1調用函數fun
fun2(fun, 200, 200); //函數fun2調用函數fun
}
編譯並運行:
yngzmiao@yngzmiao-virtual-machine:~/test$ g++ main.cc -o main -std=C++11
yngzmiao@yngzmiao-virtual-machine:~/test$ ./main
200
400
可以看出,函數指針作爲參數,可以在調用函數中調用函數指針代表的函數內容。
lambda表達式
lambda表達式就是一段可調用的代碼。主要適合於只用到一兩次的簡短代碼段。由於lambda是匿名的,所以保證了其不會被不安全的訪問:
# include <iostream>
int fun3(int x, int y){
auto f = [](int x, int y) { return x + y; }; //創建lambda表達式,如果參數列表爲空,可以省去()
std::cout << f(x, y) << std::endl; //調用lambda表達式
}
int main(){
fun3(300, 300);
}
關於lamdba表達式的內容,可以參考博文:C++ 11 Lambda表達式。
bind對象
std::bind可以用來生產,一個可調用對象來適應原對象的參數列表。具體的內容會在下文講解。
函數對象
重載了函數調用運算符()
的類的對象,即爲函數對象。
std::function
由上文可以看出:由於可調用對象的定義方式比較多,但是函數的調用方式較爲類似,因此需要使用一個統一的方式保存可調用對象或者傳遞可調用對象。於是,std::function
就誕生了。
std::function是一個可調用對象包裝器,是一個類模板,可以容納除了類成員函數指針之外的所有可調用對象,它可以用統一的方式處理函數、函數對象、函數指針,並允許保存和延遲它們的執行。
定義function的一般形式:
# include <functional>
std::function<函數類型>
例如:
# include <iostream>
# include <functional>
typedef std::function<int(int, int)> comfun;
// 普通函數
int add(int a, int b) { return a + b; }
// lambda表達式
auto mod = [](int a, int b){ return a % b; };
// 函數對象類
struct divide{
int operator()(int denominator, int divisor){
return denominator/divisor;
}
};
int main(){
comfun a = add;
comfun b = mod;
comfun c = divide();
std::cout << a(5, 3) << std::endl;
std::cout << b(5, 3) << std::endl;
std::cout << c(5, 3) << std::endl;
}
std::function可以取代函數指針的作用,因爲它可以延遲函數的執行,特別適合作爲回調函數
使用。它比普通函數指針更加的靈活和便利。
故而,std::function的作用可以歸結於:
- std::function對C++中各種可調用實體(普通函數、Lambda表達式、函數指針、以及其它函數對象等)的封裝,形成一個新的可調用的std::function對象,簡化調用;
- std::function對象是對C++中現有的可調用實體的一種類型安全的包裹(如:函數指針這類可調用實體,是類型不安全的)。
類型安全的介紹:C++類型安全
std::bind
std::bind可以看作一個通用的函數適配器,它接受一個可調用對象,生成一個新的可調用對象來適應原對象的參數列表。
std::bind將可調用對象與其參數一起進行綁定,綁定後的結果可以使用std::function保存。std::bind主要有以下兩個作用:
- 將可調用對象和其參數綁定成一個仿函數;
- 只綁定部分參數,減少可調用對象傳入的參數。
調用bind的一般形式:
auto newCallable = bind(callable, arg_list);
該形式表達的意思是:當調用newCallable
時,會調用callable
,並傳給它arg_list
中的參數。
需要注意的是:arg_list中的參數可能包含形如_n的名字。其中n是一個整數,這些參數是佔位符,表示newCallable的參數,它們佔據了傳遞給newCallable的參數的位置。數值n表示生成的可調用對象中參數的位置:_1爲newCallable的第一個參數,_2爲第二個參數,以此類推。
直接文字可能不那麼生動,不如看代碼:
#include <iostream>
#include <functional>
class A {
public:
void fun_3(int k,int m) {
std::cout << "print: k = "<< k << ", m = " << m << std::endl;
}
};
void fun_1(int x,int y,int z) {
std::cout << "print: x = " << x << ", y = " << y << ", z = " << z << std::endl;
}
void fun_2(int &a,int &b) {
++a;
++b;
std::cout << "print: a = " << a << ", b = " << b << std::endl;
}
int main(int argc, char * argv[]) {
//f1的類型爲 function<void(int, int, int)>
auto f1 = std::bind(fun_1, 1, 2, 3); //表示綁定函數 fun 的第一,二,三個參數值爲: 1 2 3
f1(); //print: x=1,y=2,z=3
auto f2 = std::bind(fun_1, std::placeholders::_1, std::placeholders::_2, 3);
//表示綁定函數 fun 的第三個參數爲 3,而fun 的第一,二個參數分別由調用 f2 的第一,二個參數指定
f2(1, 2); //print: x=1,y=2,z=3
auto f3 = std::bind(fun_1, std::placeholders::_2, std::placeholders::_1, 3);
//表示綁定函數 fun 的第三個參數爲 3,而fun 的第一,二個參數分別由調用 f3 的第二,一個參數指定
//注意: f2 和 f3 的區別。
f3(1, 2); //print: x=2,y=1,z=3
int m = 2;
int n = 3;
auto f4 = std::bind(fun_2, std::placeholders::_1, n); //表示綁定fun_2的第一個參數爲n, fun_2的第二個參數由調用f4的第一個參數(_1)指定。
f4(m); //print: a=3,b=4
std::cout << "m = " << m << std::endl; //m=3 說明:bind對於不事先綁定的參數,通過std::placeholders傳遞的參數是通過引用傳遞的,如m
std::cout << "n = " << n << std::endl; //n=3 說明:bind對於預先綁定的函數參數是通過值傳遞的,如n
A a;
//f5的類型爲 function<void(int, int)>
auto f5 = std::bind(&A::fun_3, &a, std::placeholders::_1, std::placeholders::_2); //使用auto關鍵字
f5(10, 20); //調用a.fun_3(10,20),print: k=10,m=20
std::function<void(int,int)> fc = std::bind(&A::fun_3, a,std::placeholders::_1,std::placeholders::_2);
fc(10, 20); //調用a.fun_3(10,20) print: k=10,m=20
return 0;
}
編譯並運行:
yngzmiao@yngzmiao-virtual-machine:~/test$ g++ main.cc -o main -std=C++11
yngzmiao@yngzmiao-virtual-machine:~/test$ ./main
print: x = 1, y = 2, z = 3
print: x = 1, y = 2, z = 3
print: x = 2, y = 1, z = 3
print: a = 3, b = 4
m = 3
n = 3
print: k = 10, m = 20
print: k = 10, m = 20
由此例子可以看出:
- 預綁定的參數是以值傳遞的形式,不預綁定的參數要用std::placeholders(佔位符)的形式佔位,從_1開始,依次遞增,是以引用傳遞的形式;
- std::placeholders表示新的可調用對象的第幾個參數,而且與原函數的該佔位符所在位置的進行匹配;
- bind綁定類成員函數時,第一個參數表示對象的成員函數的指針,第二個參數表示對象的地址,這是因爲對象的成員函數需要有this指針。並且編譯器不會將對象的成員函數隱式轉換成函數指針,需要通過&手動轉換;
- std::bind的返回值是可調用實體,可以直接賦給std::function。