1. 程式人生 > >《深入應用C++11》筆記-std::function和bind

《深入應用C++11》筆記-std::function和bind

接下來的內容將會補充在《深入理解C++11》書中沒有涉及的一些比較常用的特性,內容取自《深入應用C++11》。
在C++中,存在“可呼叫物件(Callable Objects)”這麼一個概念。準確來說,可呼叫物件有如下幾種定義:

  • 是一個函式指標。
  • 是一個具有operator()成員函式的類物件(仿函式)。
  • 是一個可被轉換為函式指標的類物件。
  • 是一個類成員(函式)指標。
void func(void)
{
}

struct Foo
{
    void operator()(void)
    {
    }
};

struct Bar
{
    using fr_t = void
(*)(void); static void func(void) { } operator fr_t(void) { return func; } }; struct A { int a_; void mem_func(void) { } }; int main(void) { void(*func_ptr)(void) = &func; // 1. 函式指標 func_ptr(); Foo foo; // 2. 仿函式
foo(); Bar bar; // 3. 可被轉換為函式指標的類物件 bar(); void (A::*mem_func_ptr)(void) // 4. 類成員函式指標 = &A::mem_func; int A::*mem_obj_ptr // 或者是類成員指標 = &A::a_; A aa; (aa.*mem_func_ptr)(); aa.*mem_obj_ptr = 123; return 0
; }

上面對可呼叫型別的定義裡並沒有包括函式型別或者函式引用(只有函式指標)。這是因為函式型別並不能直接用來定義物件;而函式引用從某種意義上來說,可以看做一個const的函式指標。C++中的可呼叫物件雖然具有比較統一的操作形式(除了類成員指標之外,都是後面加括號進行呼叫),但定義方法五花八門。這樣在我們試圖使用統一的方式儲存,或傳遞一個可呼叫物件時,會十分煩瑣。現在,C++11通過提供std::function和std::bind統一了可呼叫物件的各種操作。

std::function
std::function是可呼叫物件的包裝器。它是一個類模板,可以容納除了類成員(函式)指標之外的所有可呼叫物件。通過指定它的模板引數,它可以用統一的方式處理函式、函式物件、函式指標,並允許儲存和延遲執行它們

#include <iostream>
#include <functional>

void func(void)
{
   std::cout << __FUNCTION__ << std::endl;
}

class Foo
{
public:
   static int foo_func(int a)
   {
      std::cout << __FUNCTION__ << "(" << a << ") ->: ";
      return a;
   }
};

class Bar
{
public:
   int operator()(int a)
   {
      std::cout << __FUNCTION__ << "(" << a << ") ->: ";
      return a;
   }
};

int main(void)
{
   std::function<void(void)> fr1 = func; // 繫結一個普通函式
   fr1();                                // func

   // 繫結一個類的靜態成員函式
   std::function<int(int)> fr2 = Foo::foo_func;
   std::cout << fr2(123) << std::endl;   // foo_func(123) ->: 123

   Bar bar;
   fr2 = bar;     // 繫結一個仿函式
   std::cout << fr2(123) << std::endl;   // operator()(123) ->: 123

   return 0;
}

從上面我們可以看到std::function的使用方法,當我們給std::function填入合適的函式簽名(即一個函式型別,只需要包括返回值和引數表)之後,它就變成了一個可以容納所有這一類呼叫方式的“函式包裝器”。

另外,std::function還能作為函式的入參以及函式指標:

class A
{
    std::function<void()> callback_;   // 函式指標

public:
    A(const std::function<void()>& f)  // 函式入參
        : callback_(f)
    {}

    void notify(void)
    {
        callback_();   // 回撥到上層
    }
};

class Foo
{
public:
    void operator()(void)
    {
        std::cout << __FUNCTION__ << std::endl;
    }
};

int main(void)
{
    Foo foo;
    A aa(foo);
    aa.notify();

    return 0;
}

並且使用std::function有個好處就是能夠將物件的成員函式作為引數,如果使用自定義的函式指標則會報錯:

using pfunc = void(*)();

class A
{
    pfunc callback_;

public:
    A(pfunc f)
        : callback_(f)
    {}

    void notify(void)
    {
        callback_();   // 回撥到上層
    }
};

class Foo
{
public:
    void operator()(void)
    {
        std::cout << __FUNCTION__ << std::endl;
    }
};

int main(void)
{
    Foo foo;
    A aa(foo);                // 編譯失敗
    aa.notify();

    return 0;
}

std::bind
std::bind用來將可呼叫物件與其引數一起進行繫結。繫結後的結果可以使用std::function進行儲存,並延遲呼叫到任何我們需要的時候。它主要有兩大作用:

  • 將可呼叫物件與其引數一起繫結成一個仿函式。
  • 將多元(引數個數為n,n>1)可呼叫物件轉成一元或者(n-1)元可呼叫物件,即只繫結部分引數。
void call_when_even(int x, const std::function<void(int)>& f)
{
    if (x % 2 == 0)
    {
        f(x);
    }
}

void output(int x)
{
    std::cout << x << " ";
}

void output_add_2(int x)
{
    std::cout << x + 2 << " ";
}

int main(void)
{
    auto func= std::bind(output, std::placeholders::_1);
    for (int i = 0; i < 10; ++i)
    {
        call_when_even(i, func);
    }
    std::cout << std::endl;   // 0 2 4 6 8

    func= std::bind(output_add_2, std::placeholders::_1);
    for (int i = 0; i < 10; ++i)
    {
        call_when_even(i, func);
    }
    std::cout << std::endl;

    return 0;                // 2 4 6 8 10
}

同樣還是上面std::function中最後的一個例子,只是在這裡,我們使用了std::bind,在函式外部通過繫結不同的函式,控制了最後的執行結果。我們使用auto型別儲存std::bind的返回結果,是因為我們並不關心std::bind真正的返回型別(實際上std::bind的返回型別是一個stl內部定義的仿函式型別),只需要知道它是一個仿函式,可以直接賦值給一個std::function。當然,這裡直接使用std::function型別來儲存std::bind的返回值也是可以的。std::placeholders::_1是一個佔位符,代表這個位置將在函式呼叫時,被傳入的第一個引數所替代。可以通過以下的例子觀察怎麼使用佔位符:

    std::bind(output, 1, 2)();    // 1 2
    std::bind(output, std::placeholders::_1, 2)(1); // 1 2
    std::bind(output, 2, std::placeholders::_1)(1); // 2 1                       
    std::bind(output, 2, std::placeholders::_2)(1);    // 編譯失敗,呼叫時沒有第二個引數
    std::bind(output, 2, std::placeholders::_2)(1, 2); // 2 2
    std::bind(output, std::placeholders::_1, std::placeholders::_2)(1, 2);    // 1 2
    std::bind(output, std::placeholders::_2, std::placeholders::_1)(1, 2);    // 2 1

bind還有一個強大之處就是可以組合多個函式。假設要找出集合中大於5小於10的元素個數應該怎麼做呢?

int main(void)
{
    std::vector<int> arr = { 0,1,2,3,4,5,6,7,8,9 };
    auto f = std::bind(std::logical_and<bool>(),
        std::bind(std::greater<int>(), std::placeholders::_1, 5),
        std::bind(std::less_equal<int>(), std::placeholders::_1, 10));
    int count = std::count_if(arr.begin(), arr.end(), f);
    std::cout << count << std::endl;

    return 0;
}

以上程式碼中,std::logical_and<bool>()是一個模板結構體成員函式,作用是返回傳入的兩個引數的邏輯與結果,例如:

std::logical_and<bool>()(1, 2);  // 返回1&&2的結果

std::logical_and<bool>()通過bind綁定了std::greater<int>()std::less_equal<int>(),就能得到他們兩個返回值的邏輯與結果。而std::greater<int>()std::less_equal<int>()也是模板結構體成員函式,第一個引數是被比較值,綁定了第一個入參,第二引數是比較值,通過兩個引數比較返回bool值。接著講bind返回的函式,傳入std::count_if,它的作用是遍歷指定的一組資料並傳入指定函式,函式返回true則count+1。這樣就能得到集合中大於5小於10的元素個數了。

以上例子只是為了說明bind組合函式的方法,實際上用lambda函式實現更為簡單:

int main(void)
{
    std::vector<int> arr = { 0,1,2,3,4,5,6,7,8,9 };
    int count = std::count_if(arr.begin(), arr.end(), [](int x) {
        return x > 5 && x < 10;
    });
    std::cout << count << std::endl;

    return 0;
}