1. 程式人生 > >C++ 之std::function() 作為函式引數入口 詳解

C++ 之std::function() 作為函式引數入口 詳解

1. 關於std::function()

在C語言的時代,我們可以使用函式指標來吧一個函式作為引數傳遞,這樣我們就可以實現回撥函式的機制。到了C++11以後在標準庫裡引入了std::function模板類,這個模板概括了函式指標的概念
函式指標只能指向一個函式,而std::function物件可以代表任何可以呼叫的物件,比如說任何可以被當作函式一樣呼叫的物件。
當你建立一個函式指標的時候,你必須定義這個函式簽名(表徵這個函式的入參,返回值等資訊);同樣的,當你建立一個std::function物件的時候,你也必須指定它所代表的可呼叫物件的函式簽名。這一點可以通過std::function的模板引數來實現。
舉個例子來說,如果要定義一個std::function物件func,這個物件可以表示任何有如下函式簽名的可呼叫物件的,

bool(const std::unique_ptr<Widget>)&,     // C++11裡面用來比較兩個
        const std::unique_ptr<Widget>&)   //std::unique_ptr<Widget>物件的函式簽名

你可以這麼寫,

std::function<bool(const std::unique_ptr<Widget>&, 
                     const  std::unique_ptr<Widget>&)> func;

這是因為lambda表示式產生了可呼叫的物件,這個物件這裡稱做一個閉包(closure),可以儲存在std::function物件裡面。
closure(閉包)的定義是,一個函式和它所引用的非本地變數(非lambda表示式內部定義的變數)的一個集合。

2. 使用std::function作為函式入參

2.1 基於傳值的方式傳遞引數

參看下面一段程式碼,實現了一個註冊回撥函式的機制,

#include <functional>
void registerCallBack(std::function<void()>);

入參std::function<void()>是一個模板類物件,它可以用一個函式簽名為void()的可呼叫物件來進行初始化;上述實現裡面是一個傳值呼叫。我們來看一下它的呼叫過程,

// 方法(A)
registerCallBack([=]{
    ....  // 回撥函式的實現部分
})

這裡使用了lambda表示式作為函式的入參,正如前面所說的lambda表示式會生成一個匿名的閉包(closure),基於這個閉包構造了一個std::function<void()>的物件,然後通過傳值呼叫的方式把這個物件傳遞registerCallBack函式中使用。

2.2 基於引用的方式傳遞引數

當然我們還可以如下實現這個註冊函式,入參通過const引用的方式傳遞,這裡的引用必須是const的,這是因為呼叫registerCallBack函式的地方生成了一個臨時的std::function()物件,是一個右值,否則編譯會報錯。

//方法(B)
void registerCallBack(std::function<void()> const&)

這兩者的區別就在於,在registerCallBack函式內部怎麼使用這個入參,如果只是簡單的呼叫一下std::func()類,那麼兩種都沒有問題,可能使用引用的效率更高;如果register函式內部需要儲存這個std::func(),並用於以後使用,那麼方法A直接儲存沒有問題,方法B就必須做一次拷貝,否則方法B中,當臨時的物件銷燬時,有可能出現引用懸空的問題。

2.3 傳值方式下的std::function物件儲存

如果我們要在registerCallBack函式內部儲存這個傳入的function物件,我們可以使用轉移操作std::move,這樣的效率更高,

class CallBackHolder 
{
public:
  void registerCallBack(std::function<void()> func)
  {
    callback = std::move(func);
  } 
private:
  std::function<void()> callback; 
}

3. 類的成員函式作為函式入參

類的成員函式都會預設有個隱藏的this指標,所以不像普通的函式直接作為入參就可以了。

3.1 使用std::bind()和std::function來實現

std::function是通用的多型函式封裝器,它的例項可以儲存、複製以及呼叫任何可以呼叫的目標:函式,lambda表示式/bind表示式或其他函式物件,還有指向成員函式指標和指向資料成員指標;
std::bind接受一個函式(或者函式物件),生成一個重新組織的函式物件;
看下面一個例子,classA提供了一個註冊函式,用來註冊一個回撥函式

class classA
{
typedef std::function<void(int i)> callback_t;
...
    void registCb(callback_t func)
    {cbHandle = std::move(func);}
private:
    callback_t cbHandle;
};

另一個類classB需要註冊自己的一個成員函式作為回撥函式到classA中,這裡就可以使用std::bind函式來實現,

class classB
{
public:
    classB(classA& cA) 
    {       
        cA.registCb(std::bind(&classB::handle, this, std::placeholders::_1));
    }
};
  • bind函式中顯示的傳遞classB的this指標作為第一個引數給回撥函式;
  • std::placeholders:_1代表一個佔位符,用於回撥函式顯式的入參;

Effective Modern C++中專門有一節解釋過std::bind的方式比較繁瑣,並且有時侯會有一些侷限性,所以在引入了lambda表示式後就可以用lambda表示式來替代std::bind實現函式回撥註冊。

3.2 使用lambda表示式實現

使用lambda表示式的方式可以簡化這一個過程,參看如下一段程式碼,classB註冊一個成員函式作為回撥函式到classA中,classA會儲存這個回撥函式(std::function物件)到成員變數中,用於後面使用,

#include <iostream>
#include <functional>
#include <memory>

class classA
{
typedef std::function<void(int i)> callback_t;

public:
    classA() {}
    ~classA() {}
    
    void handle(int i)
    {
        std::cout << "classA::handle" << std::endl;
        
        cbHandle(i);
    }

    void registCb(callback_t func)
    {cbHandle = std::move(func);}
private:
    callback_t cbHandle;
};

class classB
{
public:
    classB(classA& cA) 
    {       
        cA.registCb([this](int i){classB::handle(i);});
    }
    ~classB() {}
    
    void handle(int i)
    {
        std::cout << "classB, handle message" << i << std::endl;
    }
};

int main()
{
    classA testa;
    classB testb(testa);

    testa.handle(10);
}
  • lambda表示式中捕獲了classB的this指標
  • 使用std::move的方式儲存function物件到classA中

 

轉自:https://www.jianshu.com/p/c4c84b073413