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 = ½ // 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);