cpp 模板泛型編程
cpp 比 c 方便不少不光因爲其支持面向對象支持class
,同樣還因爲其支持泛型編程,有方便的STL
庫。泛型要比宏強大的多,是一種設計更巧妙的編譯期動態機制,類型安全,使得一些通用算法的封裝變得十分方便。模板操作的是類型,特化的時候編譯器會做類型推導,這是模板一個核心特徵。
根據C++標準,當一個模板不被用到時它就不應該被具體化。對於cpp
編譯器是如何特化,編譯成最終代碼,用到了 惰性求值 和 模式匹配。這篇文章簡單介紹了這兩個原理:學習Haskell。
對於實際的使用,記住下面兩點:
- 函數模板的模板參數是隱式的,編譯器根據傳入值的類型來推導模板參數的類型,函數模板的參數不能有默認值。
- 類模板的模板參數是顯式的,使用一個類模板時必須指明其使用的參數,類模板的模板參數可以有默認值。
這裏主要說一下 C++ 標準庫,尤其是 STL
涉及比較操作時的常用比較器。這裏是STL源代碼。
對 sort, stable_sort 傳定製比較函數
傳入函數編譯器來做類型推導,進行模板特化。看sort
函數的模板聲明:
// 可以看出,排序要求容器支持隨機訪問迭代器,類似於數組的那種下標偏移訪問
// 這裏 _Compare 是類型, __comp 是實例,調用 sort 需要傳入的就是 __comp 實例
template <class _RandomAccessIter, class _Compare>
inline void sort(_RandomAccessIter __first, _RandomAccessIter __last,
_Compare __comp)
一個例子如下:
typedef struct tagNode {
int index;
int value;
} Node;
bool comp(const Node &a, const Node &b)
{
return a.value > b.value;
}
int main()
{
Node a[5] = { {1, 11}, {3, 33}, {2, 22}, {5, 55}, {4, 44} };
// 編譯器會進行類型推導做模板特化 <class _RandomAccessIter, class _Compare>
sort(a, a + 5, comp);
return 0;
}
對 map, set, priority_queue 傳入仿函數
仿函數functor的英文解釋爲something that performs a function,即其行爲類似函數的東西。C++中的仿函數是通過在類中重載()運算符實現,使你可以像使用函數一樣來創建類的對象。要求傳入仿函數的地方也很好理解,一般C++模板,尖括號<>
裏面放的是類型,自然這需要比較器的時候傳入的也是比較器的類型,這就是用到仿函數的地方。
首先,看一下 STL
源碼的模板聲明:
// map 和 set 底層存儲結構式都是紅黑樹
// Forward declarations of operators == and <, needed for friend declarations.
template <class _Key, class _Tp,
class _Compare __STL_DEPENDENT_DEFAULT_TMPL(less<_Key>),
class _Alloc = __STL_DEFAULT_ALLOCATOR(_Tp) >
class map;
template <class _Key, class _Compare __STL_DEPENDENT_DEFAULT_TMPL(less<_Key>),
class _Alloc = __STL_DEFAULT_ALLOCATOR(_Key) >
class set;
// priority_queue 有優先級,要求元素可比較。queue 和 priority_queue 默認的底層存儲結構也不同
// queue 默認用的是 deque 雙端隊列,priority_queue 用的是 vector
// priority_queue 實現使用的默認比較是 operator< ,是最大堆數據結構,即隊列頭元素值最大
template <class _Tp,
class _Sequence __STL_DEPENDENT_DEFAULT_TMPL(deque<_Tp>) >
class queue;
template <class _Tp,
class _Sequence __STL_DEPENDENT_DEFAULT_TMPL(vector<_Tp>),
class _Compare
__STL_DEPENDENT_DEFAULT_TMPL(less<typename _Sequence::value_type>)
class priority_queue; // 注意點:如果自己傳入自己的仿函數比較,那麼第二個存儲類型也要傳入不能缺省
對於 priority_queue
用法,這篇文章裏的例子很不錯STL裏的priority_queue用法。
一個注意點就是:如果使用自定義比較,使用仿函數,那麼使用時必然也要傳入第二個模板類型參數,要麼都缺省,要麼都傳入。下面給一個例子:
#include <queue>
#include <cstdio>
using namespace std;
typedef struct tagNode
{
int index;
int value;
} Node;
/* 針對某種類型的自定義比較仿函數,cpp 中 struct 相當於全部 public 的 class */
struct classcomp
{
/* const 這裏編譯沒有約束,但是對於明確的不可變參加上更嚴謹 */
bool operator() (const Node& a, const Node& b) const
{
return a.value > b.value;
}
};
int main()
{
Node a[5] = { {1, 11}, {3, 33}, {2, 22}, {5, 55}, {4, 44} };
// classcomp 必須是針對 Node 類型的比較仿函數,第二個缺省存儲結構也不能少
priority_queue<Node, vector<Node>, classcomp> priQue;
for(unsigned int i = 0; i < sizeof(a)/sizeof(a[0]); ++i) {
priQue.push(a[i]);
}
while(!priQue.empty()) {
const Node& topNode = priQue.top();
// Node topNode = priQue.top() // 也可以結構體直接 = 號賦值,底層相當於 memcpy, Linux 內核就有這麼用的
// const Node *ptopNode = &priQue.top(); // 如果不想拷貝,cpp 推薦用引用, 即上面的寫法
printf("index is %d, value is %d\n", topNode.index, topNode.value);
priQue.pop();
}
return 0;
}
重載 <
運算符
重載<
運算符,這種方式通用性會比較強,這種方式使得複雜結構變得像基本數據類型一樣可比較了。這樣就不用傳比較器了,因爲默認的比較器這時已經生效了。對於 cpp 代碼,重載運算符不管對於 struct 還是 class 都是很方便的,都是在自己定義的結構體或類類型里加一個重載的函數即可。這種方法缺點也是比較明顯的,如果有兩個模板容器對同一自定義數據結構(結構體或者類)需要不同的比較器,那麼重載 <
這種方法就沒有上面的自定義比較器(比較函數或者仿函數)適用了。
同樣給個例子:
#include <queue>
#include <cstdio>
using namespace std;
// 重載運算符 讓cpp代碼的表達力有很大提升,比如map重載[]可以翻遍用[]獲取指定key的value
// 還有如果定義了一些矩陣運算什麼的,重載 * 就更加方便靈活了
struct Node
{
int index;
int value;
/* 注意點:這裏的 const 不可少編譯約束 */
bool operator < (const Node& compNode) const
{
return this->value < compNode.value;
}
};
int main()
{
Node a[5] = { {1, 11}, {3, 33}, {2, 22}, {5, 55}, {4, 44} };
priority_queue<Node> priQue; //Node 類型重載了 < ,變得像 int 等基本數據類型一樣可比較了
for(unsigned int i = 0; i < sizeof(a)/sizeof(a[0]); ++i) {
priQue.push(a[i]);
}
while(!priQue.empty()) {
const Node& topNode = priQue.top();
printf("index is %d, value is %d\n", topNode.index, topNode.value);
priQue.pop();
}
return 0;
}
總結
對於 cpp 中需要自定義比較時,如果以後的操作比較都是定的,可以用重載,否則還是用自定義比較函數和仿函數。還有就是STL
中優先級隊列priority_queue
, 自定義仿函數是模板的第三個類型,如果自定了那麼第二個模板參數也要傳入,一般用vector
就可以。