05 | 容器彙編 II:需要函式物件的容器
函式物件及其特化
在講容器之前,我們需要首先來討論一下兩個重要的函式物件,less 和 hash。
我們先看一下 less,小於關係。在標準庫裡,通用的 less 大致是這樣定義的:
template <class T> struct less : binary_function<T, T, bool> { //繼承binary_function模板類,具體可參考侯捷老師的教程 bool operator()(const T& x, const T& y) const { return x < y; } };
也就是說,less 是一個函式物件,並且是個二元函式,執行對任意型別的值的比較,返回布林型別。作為函式物件,它定義了函式呼叫運算子(operator()),並且預設行為是對指定型別的物件進行 < 的比較操作。
計算雜湊值的函式物件 hash 就不一樣了。它的目的是把一個某種型別的值轉換成一個無符號整數雜湊值,型別為 size_t。它沒有一個可用的預設實現。
對於常用的型別,系統提供了需要的特化
template <class T> struct hash; template <> struct hash<int> : public unary_function<int, size_t> { size_t operator()(int v) const noexcept { return static_cast<size_t>(v); } };
我們用下面這個例子來加深一下理解:
#include <algorithm> // std::sort #include <functional> // std::less/greater/hash #include <iostream> // std::cout/endl #include <string> // std::string #include <vector> // std::vector #include "output_container.h" using namespace std; int main() { // 初始陣列 vector<int> v{13, 6, 4, 11, 29}; cout << v << endl; // 從小到大排序 sort(v.begin(), v.end()); cout << v << endl; // 從大到小排序 sort(v.begin(), v.end(), greater<int>()); cout << v << endl; cout << hex; auto hp = hash<int*>(); //c++11新特性 自動推導一個函式物件 cout << "hash(nullptr) = " << hp(nullptr) << endl; cout << "hash(v.data()) = " << hp(v.data()) << endl; cout << "v.data() = " << static_cast<void*>(v.data()) << endl; auto hs = hash<string>(); cout << "hash(\"hello\") = " << hs(string("hello")) << endl; cout << "hash(\"hellp\") = " << hs(string("hellp")) << endl; }
在 MSVC 下的某次執行結果如下所示:
{ 13, 6, 4, 11, 29 }
{ 4, 6, 11, 13, 29 }
{ 29, 13, 11, 6, 4 }
hash(nullptr) = a8c7f832281a39c5
hash(v.data()) = 7a0bdfd7df0923d2
v.data() = 000001EFFB10EAE0
hash("hello") = a430d84680aabd0b
hash("hellp") = a430e54680aad322
對於容器也是如此,函式物件的型別確定了容器的行為。
priority_queue
priority_queue 也是一個容器介面卡。上一講沒有和其他容器介面卡一起講的原因就在於它用到了比較函式物件(預設是 less)。它和 stack 相似,支援 push、pop、top 等有限的操作,但容器內的順序既不是後進先出,也不是先進先出,而是(部分)排序的結果。在使用預設的 less 作為其 Compare 模板引數時,最大的數值會出現在容器的“頂部”。如果需要最小的數值出現在容器頂部,則可以傳遞 greater 作為其 Compare 模板引數。
關聯容器
關聯容器有 set(集合)、map(對映)、multiset(多重集)和 multimap(多重對映)。跳出 C++ 的語境,map(對映)的更常見的名字是關聯陣列和字典 [3],而在 JSON 裡直接被稱為物件(object)。在 C++ 外這些容器常常是無序的;在 C++ 裡關聯容器則被認為是有序的。
關聯容器是一種有序的容器。名字帶“multi”的允許鍵重複,不帶的不允許鍵重複。set 和 multiset 只能用來存放鍵,而 map 和 multimap 則存放一個個鍵值對。
與序列容器相比,關聯容器沒有前、後的概念及相關的成員函式,但同樣提供 insert、emplace 等成員函式。此外,關聯容器都有 find、lower_bound、upper_bound 等查詢函式,結果是一個迭代器
- find(k) 可以找到任何一個等價於查詢鍵 k 的元素(!(x < k || k < x))
- lower_bound(k) 找到第一個不小於查詢鍵 k 的元素(!(x < k))
- upper_bound(k) 找到第一個大於查詢鍵 k 的元素(k < x)
如果你需要在 multimap 裡精確查詢滿足某個鍵的區間的話,建議使用 equal_range,可以一次性取得上下界(半開半閉)。如下所示:
#include <tuple>
multimap<string, int>::iterator
lower, upper;
std::tie(lower, upper) =
mmp.equal_range("four");
如果在宣告關聯容器時沒有提供比較型別的引數,預設使用 less 來進行排序。如果鍵的型別提供了比較算符 < 的過載,我們不需要做任何額外的工作。否則,我們就需要對鍵型別進行 less 的特化,或者提供一個其他的函式物件型別。
對於自定義型別,我推薦儘量使用標準的 less 實現,通過過載 <(及其他標準比較運算子)對該型別的物件進行排序。儲存在關聯容器中的鍵一般應滿足嚴格弱序關係(strict weak ordering;)
- 對於任何該型別的物件 x:!(x < x)(非自反)
- 對於任何該型別的物件 x 和 y:如果 x < y,則 !(y < x)(非對稱)
- 對於任何該型別的物件 x、y 和 z:如果 x < y 並且 y < z,則 x < z(傳遞性)
- 對於任何該型別的物件 x、y 和 z:如果 x 和 y 不可比(!(x < y) 並且 !(y < x))並且 y 和 z 不可比,則 x 和 z 不可比(不可比的傳遞性)
無序關聯容器
這些容器和關聯容器非常相似,主要的區別就在於它們是“無序”的。這些容器不要求提供一個排序的函式物件,而要求一個可以計算雜湊值的函式物件。你當然可以在宣告容器物件時手動提供這樣一個函式物件型別,但更常見的情況是,我們使用標準的 hash 函式物件及其特化。