lambda 與 priority_queue 以及 function 以及 bind


lambda是一種可調用對象,它是一個對象,每個lambda都有自己不同的類型。

年輕時以為STL和lambda混用時會有一些奇怪現象,比如我無法像這樣定義優先隊列:

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

但是卻可以這樣用sort

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 :

#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的類型

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

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

所以要實例化一個優先隊列,尖括號里得填typename啊, 由於 lambda 對象的類型是匿名的,所以用decltype搞定,再然后光這樣是不行的,這會來看構造函數:

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

可以看到,如果我們構造時,不指定特定的compare對象,那么就用typename Compare的默認構造函數構造一個,然而lambda表達式的匿名類型是沒有默認構造函數的,

所以想要正確初始化這個優先隊列,還得在構造函數里再把lambda表達式本身傳進去

 

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);


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM