【C++】C++11的std::function和std::bind用法詳解

在設計回調函數的時候,無可避免地會接觸到可回調對象。在C++11中,提供了std::functionstd::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的作用可以歸結於:

  1. std::function對C++中各種可調用實體(普通函數、Lambda表達式、函數指針、以及其它函數對象等)的封裝,形成一個新的可調用的std::function對象,簡化調用
  2. 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

相關閱讀

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章