STL中很多算法都要對迭代器范圍內的元素做特定操作,這些操作是需要用戶顯示傳遞給迭代器,如何才能有效地傳遞這些操作呢?STL為我們提供了函數對象來解決這個問題。本文先簡要介紹函數對象的概念,然后配合源代碼介紹STL為我們提供的幾種函數對象,最后介紹強大的函數對象適配器。
0 函數對象
標准庫的很多算法需要一些指向序列的迭代器對該序列作操作,算法的參數除了迭代器以外還會需要用戶指定一些值或指定一些操作,以應用到迭代器指向的元素上。
例如要用6替代某vector中的所有值為3的元素:
1 Void func(vector<int>& c) 2 { 3 replace(c.begin(), c.end(), 3, 6); 4 }
但是,有時候,我們需要替代序列中滿足某些條件的元素,就像下面一樣:
1 bool bigthan5(int v) 2 { 3 return v>5; 4 } 5 replace_if(vec.begin(), vec.end(), bigthan5, 8);
像上面這樣,存在很多為算法傳遞函數(如謂詞演算,邏輯運算等)的情形,如果每次都需要用手手動去編寫傳遞給算法的函數既不方便也不有效。相對來說,類的成員函數能更好地提供這種服務,因為類的對象可以保存數據,而且,類還能作一些初始化數據的工作。
例如對於上面的條件替換,我們可以像下面這樣用:
1 template<class T> 2 class Bigthan5 3 { 4 public: 5 bool operator()(T& v){return v>5;} 6 }; 7 8 Bigthan5<int> s; 9 repace_if(vec.begin(), vec.end(), s, 8); //將vec中所有值大於5的元素替換為8
在上面的replace_if()中,對於vec.begin()和vec.end()范圍內的所有元素都調用了函數Bigthan5<int>::operator()(int)函數。
上面的類Bigthan5重載了opearator()操作符。運算符()的典型應用是為對象提供常規的函數調用的語法形式,使他們具有像函數一樣的行為方式,一個應用起來像函數的對象被稱作一個擬函數對象,簡稱函數對象。
《C++程序設計語言》中指出:一個函數對象的開銷比指針傳遞函數的開銷小,所以函數對象通常比常規函數執行速度更快。
標准庫為我們提供了豐富的函數對象
1,函數對象的基類
標准庫提供了兩種函數對象基類:
1 template <class _Arg, class _Result> 2 struct unary_function { 3 typedef _Arg argument_type; 4 typedef _Result result_type; 5 }; 6 7 template <class _Arg1, class _Arg2, class _Result> 8 struct binary_function { 9 typedef _Arg1 first_argument_type; 10 typedef _Arg2 second_argument_type; 11 typedef _Result result_type; 12 };
其中,類unary_function應用於一元操作符,它會接受兩個模板參數,一個作為其返回值類型,另一個作為其參數類型;相應地,binary_function應用於二元操作符。
2,算術函數對象及其應用
STL提供了幾種算術函數對象,分別是:plus, minus, mutiplies, divides, modulus, negate。它們長得都差不多,類似於下面的樣子:
1 template <class _Tp> 2 struct divides : public binary_function<_Tp,_Tp,_Tp> { 3 _Tp operator()(const _Tp& __x, const _Tp& __y) const { return __x / __y; } 4 };
需要注意的是,這些算術函數對象的參數類型和返回值類型是一樣的,這個不難理解。
我們可以像下面這樣用算術函數對象:
int result = accumulate(vec.begin(), vec.end(), 1, mutiplies<int>());
該函數將vec.begin()和vec.end()之間的元素累積相乘。
3,謂詞函數對象及其應用
STL提供了一下幾種謂詞函數對象: equal_to, not_equal_to, greater, less, greater_equal, less_equal, logical_and, logical_or, logical_not。它們長得都類似於下面的樣子:
1 template <class _Tp> 2 struct logical_and : public binary_function<_Tp,_Tp,bool> 3 { 4 bool operator()(const _Tp& __x, const _Tp& __y) const { return __x && __y; } 5 };
謂詞函數對象的返回值都是bool類型。
我們可以像下面這樣用謂詞函數對象:
1 sort(vec.begin(), vec.end(), less<int>());
上面的語句表示將序列vec的所有元素遞增排序。
4,用戶自定義函數對象
標准庫提供的函數對象的最大靈活性在於用戶可以自定義函數對象,當我們把算法應用於那些在設計時並沒有考慮標准庫和標准算法的類時,自定義函數對象的能力就特別重要了[1]。
《C++程序設計語言》中提供了用戶自定義謂詞函數對象的一個例子:
1 class Person {...}; 2 struct Club{ 3 string name; 4 list<Person*> members; 5 List<Person*> officers; 6 //… 7 Club(const string& s); 8 }; 9 10 Class Club_eq : public unary_function<Club, bool>{ 11 String s; 12 public: 13 explict Club_eq(const string& ss) : s(ss) {} 14 bool operator()(const Club& c) const {return c.name == s;} 15 }; 16 void f(list<Club>& lc) 17 { 18 typedef list<Club>::iterator LCI; 19 LCI p = find_If(lc.begin(), lc.end(), Club_eq("Dining Philosophers")); 20 }
一般情況下,在使用函數對象的時候,向STL算法傳遞一個對象即可,算法會自動調用該對象的operator()操作符。
5,函數對象的適配器
STL為支持標准函數對象的組合提供了標准函數對象的適配器。
這些適配器都有共同的特征:它們都依賴於函數對象基類unary_function, binary_function, 對這些適配器中的每一個都提供了一個協助函數,它以一個函數對象為參數,返回另一個合適的函數對象。也就是說,這種適配器是一種形式簡單的高階函數,它以一個函數作為參數並具此產生另一個新的函數[1]。
5.1約束器
bind1st和bind2nd可以綁定二元函數對象的某一個值。值得注意的是:bind1st和bind2nd不是函數對象,它們是普通的函數,它們的輸入參數中,第一個參數是二元函數對象,第二個參數是要綁定的值(對於bind1st是綁定二元函數參數的第一個參數,對於bind2nd是綁定二元函數參數的第二個參數)。
下面是bind1st的完整實現代碼:
1 template <class _Operation> 2 class binder1st 3 : public unary_function<typename _Operation::second_argument_type, 4 typename _Operation::result_type> { 5 protected: 6 _Operation op; 7 typename _Operation::first_argument_type value; 8 public: 9 binder1st(const _Operation& __x, 10 const typename _Operation::first_argument_type& __y) 11 : op(__x), value(__y) {} 12 typename _Operation::result_type 13 operator()(const typename _Operation::second_argument_type& __x) const { 14 return op(value, __x); 15 } 16 }; 17 18 template <class _Operation, class _Tp> 19 inline binder1st<_Operation> 20 bind1st(const _Operation& __fn, const _Tp& __x) 21 { 22 typedef typename _Operation::first_argument_type _Arg1_type; 23 return binder1st<_Operation>(__fn, _Arg1_type(__x)); 24 }
在我們最上面提供的替換大於5的元素的實現中,我們可以像下面的方式這樣調用bind1st:
1 #include <functional> 2 ... 3 replace_if(vec.begin(), vec.end(), bind1st(less<int>(), 5), 8);
5.2 成員函數適配器
有時候,用戶要給某個算法傳遞一個對象的成員函數,這個時候,我們需要用到成員函數的適配器mem_fun()和mem_fun_ref()。
其中,mem_fun()接受一個對象指針傳遞過來的成員函數,mem_fun_ref()接受一個對象引用傳遞過來的成員函數,它們都返回一個函數對象。
下面摘錄一部分mem_fun()的實現代碼:
1 //這個版本的mem_fun接受一個指針、無參、非void返回值、非const成員函數 2 // _Tp::*f 表示指向類_Tp成員函數的指針 3 template <class _Ret, class _Tp> 4 inline mem_fun_t<_Ret,_Tp> mem_fun(_Ret (_Tp::*__f)()) 5 { return mem_fun_t<_Ret,_Tp>(__f); } 6 7 template <class _Ret, class _Tp> 8 class mem_fun_t : public unary_function<_Tp*,_Ret> { 9 public: 10 explicit mem_fun_t(_Ret (_Tp::*__pf)()) : _M_f(__pf) {} 11 _Ret operator()(_Tp* __p) const { return (__p->*_M_f)(); } 12 private: 13 _Ret (_Tp::*_M_f)(); 14 };
我們可以像下面這樣使用mem_fun():
1 void draw_all(list<Shape*>& lsp){ 2 For_each(lsp.begin(), lsp.end(), mem_fun(&Shape::draw)); 3 }
mem_fun_t()和mem_fun_ref_t()函數族群共有16 = 2 ^ 4個函數,分別對應:
1)成員函數無參數 or 成員函數有一個參數
2)通過指針調用 or 通過引用調用
3)無返回值 or 有返回值
4)const成員函數 or non_const成員函數
5.3普通函數適配器
與5.2類似,有時候用戶需要給算法傳遞一個普通函數,這個時候,我們需要用到普通函數的適配器ptr_fun(),它有兩個重載版本,一個有一個參數,另一個有兩個參數。
適配器ptr_fun()返回一個函數對象
下面的代碼是擁有一個參數的普通函數適配器的實現代碼:
1 template <class _Arg, class _Result> 2 class pointer_to_unary_function : public unary_function<_Arg, _Result> { 3 protected: 4 _Result (*_M_ptr)(_Arg); 5 public: 6 pointer_to_unary_function() {} 7 explicit pointer_to_unary_function(_Result (*__x)(_Arg)) : _M_ptr(__x) {} 8 _Result operator()(_Arg __x) const { return _M_ptr(__x); } 9 }; 10 11 template <class _Arg, class _Result> 12 inline pointer_to_unary_function<_Arg, _Result> 13 ptr_fun(_Result (*__x)(_Arg)) 14 { 15 return pointer_to_unary_function<_Arg, _Result>(__x); 16 }
我們可以看到,ptr_fun接受一個函數指針x,返回一個類pointer_to_unary_function的對象,該對象重載operator()操作符時調用x傳遞過來的函數。
在我們最上面提供的替換大於5的元素的實現中,我們可以像下面的方式這樣調用bind1st:
#include <functional> ... replace_if(vec.begin(), vec.end(), ptr_fun(bigthan5), 8);
5.4謂詞否定迭代器
顧名思義,這個否定迭代器會接受任何一個返回值為bool類型的模板參數,這個模板參數可以是上面提到的任何返回值為bool的函數,也可以是用戶自定義的返回值為bool的函數,或者返回值為bool的類成員函數。
對這個bool取反之后返回一個函數對象。
否定迭代器由not1和not2組成,它們分別有一個或兩個模板參數。
下面是not2的實現代碼:
1 template <class _Predicate> 2 class binary_negate 3 : public binary_function<typename _Predicate::first_argument_type, 4 typename _Predicate::second_argument_type, 5 bool> { 6 protected: 7 _Predicate _M_pred; 8 public: 9 explicit binary_negate(const _Predicate& __x) : _M_pred(__x) {} 10 bool operator()(const typename _Predicate::first_argument_type& __x, 11 const typename _Predicate::second_argument_type& __y) const 12 { 13 return !_M_pred(__x, __y); 14 } 15 }; 16 17 template <class _Predicate> 18 inline binary_negate<_Predicate> 19 not2(const _Predicate& __pred) 20 { 21 return binary_negate<_Predicate>(__pred); 22 }
在我們最上面提供的替換元素的實現中,我們可以像下面的方式這樣調用bind1st:
1 #include <functional> 2 ... 3 replace_if(vec.begin(), vec.end(), not1(ptr_fun(bigthan5)), 8);
上面的代碼將所有小於等於5的元素替換為8。
6 總結
函數對象在STL的算法部分占有很重要的作用,STL中基本上所有的算法都像__pred(*__first)或__binary_op(*__first1, *__first2)這樣對迭代器指向的元素進行操作,這就需要用戶正確地傳遞__pred和__binary_op給算法。本節中介紹的函數對象不論在封裝性還是性能上都完全勝任這個任務。
對函數對象的幾種類型有明確的理解,並輔之以相關的練習,定會掌握它。
7 參考書目
[1] 《C++程序設計語言》 Bjarne Stroustrip
[2] SGI STL-3.3源代碼
前三節簡要介紹了STL中的內存管理、迭代器和函數對象,從下一節開始,我們將踏上美麗的容器和算法之旅。
本文為原創,轉載請注明原地址http://www.cnblogs.com/cobbliu/archive/2012/04/21/2461184.html,謝謝合作~