std::bind(一):包裝普通函數

前言

不知道大家在做項目寫程序的過程中有沒有遇到這樣的情況,別的模塊類提供了一個擁有很多參數接口函數,但是我這個功能只用到其中幾個,其他的參數都是固定的,可是爲了調用這個接口函數,不得不將所有的參數寫一遍,每次寫一堆固定參數都感覺在浪費生命。

有的人可能想到默認參數,的確,默認參數可以解決部分問題,因爲默認參數只能出現參數列表的尾部,如果4個參數中,我需要傳遞的參數是第4個,而前3個參數想默認的話,默認參數是做不到這種效果的,並且別人的接口函數也不一定會有默認參數。

函數封裝,這是一個辦法,我們在自己的模塊中添加一個對接口函數進行包裝後的函數,將不變的參數進行固定,然後只留可變的參數供我們自己調用,如果我們有3種常用的調用方式可能就需要定義3個函數,這種方法可行,不過比較麻煩,而std::bind()函數就是爲了包裝函數而生的,使用起來更加方便。

std::bind()的作用

std::bind()的作用就是對原函數進行包裝,可以將參數值固定,調換順序然後生成新的函數供我們調用。舉個例子,一塊鐵片你可以拿它來做很多事情,打造一下可以做成一把刀,敲敲打打可以做成一個桶,甚至直接拿來就可以擋窗戶上的洞。std::bind()的作用就是把這塊鐵的作用固定,比如給她安上一個刀把,這樣我們每次使用就可以把這塊鐵片當成菜刀來使用了。

std::bind()可以包裝各種函數,但是這篇文章只總結一下包裝普通函數的方法,因爲在學習的過程中我發現單單是包裝普通函數也會遇到很多問題,所以爲了列舉出諸多可能,說明各種注意事項,本文還是隻關注於普通函數的包裝,至於成員函數的包裝還是放到以後的文章,給自己埋下一個坑。

在包裝普通函數時,std::bind()的第1個參數就是原函數的名字,當然也可以是指向函數的指針,或者函數引用,從第2個參數開始,填寫的內容依次對應原函數中的各個參數,所以說如果原函數是3個參數,如果想包裝它,那麼std::bind()需要傳入4個參數,如果原函數是8個參數,那麼包裝它的std::bind()就需要傳入9個參數,這裏爲了將原函數和包裝後的函數參數建立聯繫,需要引入命名空間std::placeholders

placeholders的作用

std::placeholders的命名空間下有多個參數佔位符,比如placeholders::_1placeholders::_2等等,最大爲placeholders::_20,在包裝普通函數時,固定的參數很好說,就是填寫固定值就可以,但是要想原函數的參數和包裝後函數的參數建立聯繫就需要用到剛剛提到的佔位符, placeholders::_1就表示包裝後函數的調用時的第1個參數,同理placeholders::_2就表示包裝後函數的調用時的第2個參數。

有了佔位符的概念,我們就可以推斷出,包裝後的函數與原函數相比,不但可以減少函數參數,也可以增加函數參數,雖然暫時沒有想到什麼實際的使用場景,但是理論上是可行的。

std::bind()使用測試

首先需要先引入頭文件,免得找不到命名空間和函數定義

#include <iostream>
#include <functional>
using namespace std;

固定參數、調換順序

void func1(int n1, int n2, int n3)
{
    cout << n1 << ' ' << n2 << ' ' << n3  << endl;
}

void test1_1()
{
    auto f1 = std::bind(func1, placeholders::_1, 101, placeholders::_2);
    f1(11, 22);   // same as call func1(11, 101, 22)
}

void test1_2()
{
    auto f1 = std::bind(func1, placeholders::_2, 101, placeholders::_1);
    f1(11, 22);   // same as call func1(22, 101, 11)
}

// 輸出
//11 101 22
//22 101 11

函數test1_1()展示了std::bind()函數最常見的用法,其中參數n2被固定爲101,參數n1使用佔位符placeholders::_1表示,表示包裝後函數的第1個參數會傳給形參n1使用,同理包裝後函數的第2個參數會傳給形參n3使用,所以調用函數f1(11, 22) 就等同於調用函數 func1(11, 101, 22)test1_2()函數簡單展示了調換參數順序的方法,只要明白了placeholders的作用,這兩個例子也就明白了。

包裝後函數的參數個數可增可減

void func2(int n1, int n2, int n3)
{
    cout << n1 << ' ' << n2 << ' ' << n3 << endl;
}

void test2_1()
{
    auto f2 = std::bind(func2, placeholders::_3, 101, placeholders::_1);
    f2(11, 22, 33);   // same as call func2(33, 101, 11)
}

void test2_2()
{
    auto f2 = std::bind(func2, placeholders::_1, 101, placeholders::_1);
    f2(11);   // same as call func2(11, 101, 11)
}

void test2_3()
{
    auto f2 = std::bind(func2, placeholders::_1, 101, placeholders::_2);
    f2(11);   // 報錯,因爲沒有參數傳給placeholders::_2
}

// 輸出
//33 101 11
//11 101 11
//編譯錯誤

其實在理解了placeholders的作用之後,這個測試結果也能想到的,函數test2_1()中使用了placeholders::_3,所以包裝後函數的參數至少要傳3個纔不會報錯,而test2_2()函數中使用了placeholders::_1,所以被包裝函數調用時只需要傳入一個參數,最後是函數test2_3(),綁定時引用了placeholders::_2,而在調用時只傳了一個參數,所以出現編譯錯誤。

bind()綁定時參數個數固定,類型需匹配

void func3(int n1, int n2, int n3)
{
    cout << n1 << ' ' << n2 << ' ' << n3 << endl;
}

void test3_1()
{
    auto f3 = std::bind(func3, placeholders::_1, 101);
    //f3(11);   // 編譯錯誤,因爲bind函數中少了一個參數
}

void test3_2()
{
    auto f3 = std::bind(func3, placeholders::_1, 101, 102, 103);
    //f3(11);   // 編譯錯誤,因爲bind函數中多了一個參數
}

void test3_3()
{
    auto f3 = std::bind(func3, placeholders::_1, "test", placeholders::_1);
    //f3(11);   // 編譯錯誤,第二個參數類型不匹配,無法將參數 2 從“const char *”轉換爲“int”
}

看了之前的測試之後,是不是覺得參數的個數很隨意,可以隨便增加和減少,所以在綁定的時候也不好好寫了,結果發現上述3個函數全部編譯錯誤,test3_1()函數中因爲綁定時少了一個參數而報錯,test3_2()函數中因爲綁定時多了一個參數而報錯,而test3_3()函數中因爲綁定時第二個參數的類型不匹配而報錯,所以參數個數的增減只能是包裝後的函數,而綁定時必須嚴格與原函數的參數個數以及類型相匹配。

普通函數的參數中有引用類型

弄明白上面的例子之後,可能會產生一種我會了的錯覺,想象一下如果原函數參數中包含引用類型應該怎樣寫,可以自己先想一下,然後看看下面的例子

void func4(int n1, int n2, int& n3)
{
    cout << n1 << ' ' << n2 << ' ' << n3 << endl;
    n3 = 101;
}

void test4_1()
{
    int n = 10;
    auto f4 = std::bind(func4, 11, 22, n);
    n = 33;
    f4();   // same as call func4(11, 22, 10)
    cout << "n = " << n << endl;
}

void test4_2()
{
    const int n = 30;
    auto f4 = std::bind(func4, 11, 22, n);
    f4();   // same as call func4(11, 22, 30)
}

void test4_3()
{
    int n = 30;
    auto f4 = std::bind(func4, 11, 22, ref(n));
    n = 33;
    f4();   // same as call func4(11, 22, n)
    cout << "n = " << n << endl;
}

void test4_4()
{
    const int n = 30;
    auto f4 = std::bind(func4, 11, 22, ref(n));
    //f4();   // 編譯錯誤,無法將參數 3 從“const int”轉換爲“int &”
}

// 輸出
//11 22 10
//n = 33
//11 22 30
//11 22 33
//n = 101

如果能準確說出test4_1()函數的輸出結果,那麼後面的內容應該是不需要看了,如果只回答對了部分內容,或者乾脆全錯了,那麼我們還有很長的路要走。

std::bind()的官方文檔中有這樣一句話,std::bind()函數中的參數在被複制或者移動時絕不會以引用的方式傳遞,除非你使用了std::ref()或者std::cref()包裝的參數,如果知道了這個限定,就很容易明白函數test4_1()函數的輸出結果了。

在函數test4_1()std::bind(func4, 11, 22, n)就相當於std::bind(func4, 11, 22, 10),所以輸出結果爲11 22 10,可是函數func4()中還有一句 n3 = 101;,這就很讓人奇怪了,我們知道常數是沒辦法作爲參數傳遞給可變引用變量的,如果說把10作爲參數傳遞給參數int& n3肯定會報錯,而函數test4_1()卻正常執行,沒有任何問題。

我們猜測常數10到參數int& n3並不是直接傳遞,而是發生了拷貝,而函數func4()中修改的n3變量也是修改的拷貝內容,所以我們做了test4_2()這個實驗,發現將變量n改爲常量也是可以正常執行的,甚至直接寫成std::bind(func4, 11, 22, 10)也是沒問題的,這也驗證了我們上面的想法。

既然文檔了提到了std::ref()std::cref()函數,那麼我們想傳遞引用給原函數只能使用它們了,看下函數test4_3()的實現,這纔是正確傳遞引用變量的方式,變量n被函數 std::ref() 包裝之後,既能夠感受到本函數中變量n的變化,也能夠傳入到原函數中被原函數的邏輯改變,並將結果反映回來。

函數test4_4()只是一個常量傳遞的簡單測試,將一個常量作爲可變引用變量來傳遞肯定是無法通過編譯的,這在函數調用時很明確,但是在std::bind()加入之後顯得有些混亂,只要記住一點,常量不應該被改變,如果傳遞之後內容可能會變化,那麼很可能這種寫法就是錯誤的。

總結

  1. 其實std::bind()函數測試到現在遠遠沒有結束,配合std::ref()std::cref()函數會產生多種組合情況,不過主要的問題上面都提到了一些,出現問題的時候對照着定義和概念看看應該就能理解了。

  2. 需要理解std::placeholders的佔位作用,它們是std::bind()函數最基本的用法。

完整代碼

代碼傳送門

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