lambda 與 priority_queue 以及 function 以及 bind

lambda是一種可調用對象,它是一個對象,每一個lambda都有本身不一樣的類型。ios

年輕時覺得STL和lambda混用時會有一些奇怪現象,好比我沒法像這樣定義優先隊列:c++

priority_queue<int, vector<int>, [](int a, int b) {return a > b;}> que;

可是卻能夠這樣用sortexpress

sort (vec.begin(), vec.end(), [](int a, int b) {return a < b;});

 以及能夠這樣用sort數組

bool less(int a, int b) { return a < b;}

typedef bool (*cmp) (int, int);
cmp mycmp = less;
sort (vec.begin(), vec.end(), mycmp);

sort (vec.begin(), vec.end(), less);  

 

之因此會出現這樣的疑問,是由於沒有搞清楚函數對象 (也叫可調用對象) 和 模板的類型參數之間的關係, 首先說明如何正確的使用 lambda 對象來實例化priority_queue :less

#include <iostream>
#include <queue>
#include <vector>
#include <utility>
 
using my_pair_t = std::pair<size_t,bool>;
 
using my_container_t = std::vector<my_pair_t>;
 
int main()
{
    auto my_comp =
        [](const my_pair_t& e1, const my_pair_t& e2) 
        { return e1.first > e2.first; };
    std::priority_queue<my_pair_t,
                        my_container_t,
                        decltype(my_comp)> queue(my_comp);
    queue.push(std::make_pair(5, true));
    queue.push(std::make_pair(3, false));
    queue.push(std::make_pair(7, true));
    std::cout << std::boolalpha;
    while(!queue.empty()) 
    {
        const auto& p = queue.top();
        std::cout << p.first << " " << p.second << "\n";
        queue.pop();
    }
}

1, 首先這裏的my_comp是一個對象,因爲每一個lambda對象都有一個匿名的類型,因此只能用auto來表達my_comp的類型dom

2, 優先隊列的聲明是這樣的:函數

template<
    class T,
    class Container = std::vector<T>,
    class Compare = std::less<typename Container::value_type>
> class priority_queue;

因此要實例化一個優先隊列,尖括號裏得填typename啊, 因爲 lambda 對象的類型是匿名的,因此用decltype搞定,再而後光這樣是不行的,這會來看構造函數:spa

explicit priority_queue( const Compare& compare = Compare(),
                         const Container& cont = Container() );

能夠看到,若是咱們構造時,不指定特定的compare對象,那麼就用typename Compare的默認構造函數構造一個,然而lambda表達式的匿名類型是沒有默認構造函數的,指針

因此想要正確初始化這個優先隊列,還得在構造函數裏再把lambda表達式自己傳進去c++11

 

3, 函數指針,函數, lambda表達式,函數對象之間的關係

首先看sort的一個重載聲明:

template <class RandomAccessIterator, class Compare>
  void sort (RandomAccessIterator first, RandomAccessIterator last, Compare comp);

這裏sort接受一個函數對象,因此直接給傳遞lambda對象是能夠的,由於lambda對象屬於函數對象嘛,在C++裏凡是可以使用函數調用運算符的,都是函數對象,因此

函數對象有: 函數, 函數指針, 重載了()的類的對象,以及lambda對象。

注意,函數和函數指針是兩個不一樣的東西,就像雖然數組在做爲參數時會被當成指針傳遞,可是他們依舊不是同一個東西,即:

雖然在非引用時,數組和函數都會被轉換成指針(包括模板參數推斷也會這樣),可是他們仍舊不是指針,指針的例子不用多說,舉個函數的例子:

template <typename T>
void t(const T& t) {
    cout << std::is_pointer<T>::value << endl;
    t();
}

template <typename T>
void e(T t) {
    cout << std::is_pointer<T>::value << endl;
    t();
}

void f() {
}
 int main() {
    t(f);

    cout << endl;
    e(f);
    return 0;
}

 

第一次調用輸出的是false,第二次則是true,這也就是說函數自己 和 函數指針也是兩個東西

 

至此也就解釋了一開始說到的如何使用sort以及priority_queue的緣由了

 

 

 

/*---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------*/

關於function 和 bind :

搞清楚函數對象、函數指針、函數以及lambda對象之間的關係以後,很明顯這些函數對象的類型是不同的,而且在c++11中,函數 的類型能夠由函數的調用方式來表示

好比一個函數(函數自己,而不是函數指針或者其餘什麼東西) int f (int, int) {...},那麼咱們能夠用int(int, int)來表示它的類型,可是值得注意的一點的是,若是想要聲明一個

函數,並不能直接

int(int, int) f;

不過咱們能夠這樣:

int a (int, int) {return 1;}
auto b = a;

 

關於這些東西的類型細節,咱們能夠經過下面的代碼來詳細的說明:

int a(int, int) {return 1;}
int f(int, int) { return 2; }
int main() {
    auto b = []() { return 1; };
    auto c = b;
    int (*d)(int, int);
    d = a;
    auto e = a;
    cout << boolalpha;
    cout << is_same<int(int, int), decltype(a)>::value << endl;       //true
    cout << is_same <decltype(a), decltype(f)>::value << endl;        //true
    cout << is_same<int(int, int), decltype(b)>::value << endl;       //false
    cout << is_same<int(int, int), decltype(c)>::value << endl;       //false
    cout << is_same<decltype(a), decltype(d)>::value << endl;         //false
    return 0;
}

 

那麼問題就來了, 這些可調用對象的類型都不一致,可是他們的調用方式卻能夠是一致的,好比函數 int a(int); 和 lambda [](int)->int{...}他們都接受一個int類型做爲參數,而後

返回一個int類型,但是因爲它們的類型不一致,咱們沒法在函數和lambda對象之間進行拷貝和賦值:

好比咱們沒法將lambda表達式插入到std::set<int(*)(int)>中。。。

爲了解決此類問題,有一羣變態(是的,一羣變態。。。)他們實現了一個function模板,

function模板是這樣一個東西:

function僅僅以函數對象的調用方式來區分類型,也就是說,經過decltype([](int){return 1;})實例化的function對象,和經過int(int) (即上文提到的"函數自己"的類型)實例化的function對象的類型是一致的而且能夠相互拷貝的。

具體使用方式能夠參考下面的代碼:

// function example
#include <iostream>     // std::cout
#include <functional>   // std::function, std::negate

// a function:
int half(int x) {return x/2;}

// a function object class:
struct third_t {
  int operator()(int x) {return x/3;}
};

// a class with data members:
struct MyValue {
  int value;
  int fifth() {return value/5;}
};

int main () {
  std::function<int(int)> fn1 = half;                    // function
  std::function<int(int)> fn2 = &half;                   // function pointer
  std::function<int(int)> fn3 = third_t();               // function object
  std::function<int(int)> fn4 = [](int x){return x/4;};  // lambda expression
  std::function<int(int)> fn5 = std::negate<int>();      // standard function object

  std::cout << "fn1(60): " << fn1(60) << '\n';
  std::cout << "fn2(60): " << fn2(60) << '\n';
  std::cout << "fn3(60): " << fn3(60) << '\n';
  std::cout << "fn4(60): " << fn4(60) << '\n';
  std::cout << "fn5(60): " << fn5(60) << '\n';

  

  fn1 = fn3;                                             
  std::cout << "changed fn1(60): " << fn1(60) << '\n';

// stuff with members:
  std::function<int(MyValue&)> value = &MyValue::value;  // pointer to data member
  std::function<int(MyValue&)> fifth = &MyValue::fifth;  // pointer to member function

  MyValue sixty {60};

  std::cout << "value(sixty): " << value(sixty) << '\n';
  std::cout << "fifth(sixty): " << fifth(sixty) << '\n';

  return 0;
}

 

從上面的代碼中能夠看到,只要調用方式相同,不論是類的成員指針、函數、函數指針、lambda對象,只要被function包裹以後,他們的類型就都是一致的,而且是能夠相互拷貝的 (即便他們包裹的函數對象的具體類型不一致)。可是須要注意的一點是 :由於function的構造函數中,會將函數對象的一份decay copy (即去除了const volatile 以及reference以後的拷貝) 保存到function對象內部,因此若是被包裹的函數對象不能拷貝,那麼function將會引起編譯錯誤,好比:

struct foo {
    explicit  foo(int) { p = 1;}
    foo (const foo&) = delete;
  //  foo(const foo&) { static int copyCount = 0; cout << ++copyCount << endl; }; // 會發生三次拷貝
    int operator()(int) {return 1;}
    int p;
};

int main() {
    function<int(int)> fn1 = foo(1);
    function<int(int)> fn2 = foo(2);
    fn1 = fn2;
    return 0;
}

上面的代碼會發生編譯錯誤: 引用了deleted 的拷貝構造函數,進一步的,若是把拷貝構造函數換成註釋掉的部分,能夠看到發生了三次拷貝。

 

 

/*---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------*/

關於bind:

在瞭解了各類函數對象的細節以後,考慮當可調用對象有默認參數時的情景,如:

int f(int, int arg2 = 0);

此時f 的類型是 int(int, int) (能夠驗證,此處再也不貼代碼證實),而f的調用方式卻能夠是這樣的 f(0)

這個時候bind就派上用場了,咱們能夠經過 bind 將默認參數綁定到某個可調用對象上,從而獲得一個全新的,調用方式不同的可調用對象,bind 是一種給可調用對象加上默認參數

的手法, 可是bind比默認參數更強大, bind能夠顯式的改變函數對象的 調用方式,以更好的配合 function 和 STL 的組件, bind 不但能夠設置默認參數,還能改變參數的位置, bind的

通常形式爲:

auto newCallable =  bind (callable, arg_list)

更具體的:

auto newCallable = bind (callable, _2, arg2, _1)

這意味着咱們調用 newCallable(1, 2) 時, 等價於 callabe(2, arg2, 1), 而且此時 newCallalbe 的調用方式 也已經變成相似於 int (int, int)這樣只接受兩個參數的類型了。

上面的_1, _2叫作佔位符(placeholders), 屬於命名空間 std::placeholders,其中

_1 表示 newCallable的第一個參數, _2 表示 newCallable 的第二個參數,以此類推。

 

值得注意的是:

bind 對於非佔位符的傳遞方式是拷貝!!好比對於沒法拷貝的cout流,咱們只能這樣寫bind

 auto newCallable = bind (callable, _2, std::ref(arg2), _1);

相關文章
相關標籤/搜索