C++ unordered_multimap用法詳解

2020-07-16 10:04:31
unordered_multimap 是一個允許有重複鍵的無序 map。因此,它支援的操作實際上和 unordered_map 容器是相同的,為了處理多個重複鍵所做的新增和更改除外。後面會對這些差別做些討論。生成 unordered_multimap 的方式和 unordered_map 相同。例如:
std:: unordered_multimap<std::string,size_t> people {{"Jim",33}, {"Joe",99}};
可以使用 insert()、emplace()、emplace_hint() 來新增新元素,這和 unordered_multimap 相同,只要引數和容器中的元素型別一致。這些成員函數都會返回一個指向容器中新元素的疊代器;在使用 inserted emplace() 的情況下,unordered_multimap 和 unordered_map 有些不同,unordered_multimap 的這兩個函數會返回一個 pair 物件,它用來說明插入是否成功。如果不成功,也是一個疊代器。例如: 
auto iter = people.emplace("Jan", 45);
people.insert({"Jan", 44});
people.emplace_hint(iter, "Jan", 46);
第 3 條語句使用了第 1 條語句返回的疊代器作為插入元素的提示符。這個提示符有時會被容器或我們的實現所忽略。

unordered_map 支援的成員函數 at() 和 operator[]() 對於 unordered_multimap 來說並不可用,因為潛在的重複鍵。唯一的選擇是使用 find() 和 equal_range() 來存取元素。find() 總會返回它所找到的第一個元素的疊代器,如果找不到這個鍵,會返回一個結束疊代器。可以以鍵為引數呼叫 count() 來發現容器中給定鍵的元素個數。下面展示實際用法:
std:: string key{"Jan"};
auto n = people.count(key);//Number of elements stored with key
if(n == 1)
    std::cout << key << " is " << people.find(key)->second<<std::endl;
else if (n > 1)
{
    auto pr = people.equal_range (key); // pair of begin & end iterators returned
    while(pr.first != pr.second)
    {
        std::cout << key << " is " << pr.first->second << std::endl;
        ++pr.first; // Increment begin iterator
    }
}
當容器中只有一個 key 時,可以用 find() 來存取這個元素。如果超過一個,可以用 equal_range() 來存取這段元素。當然,在這兩種情況下都可以使用 equal_range()。

讓我們來看一個使用 unordered_multimap 的範例。在這個範例中也會展示一些定義函數模板來使用容器的方式。這個程式會實現一個電話簿,可以用姓名或名稱來查詢電話號碼。此處用一個 pair 物件來封裝名和姓,用一個元組來記錄區號、交換碼、電話號碼,它們都是以 string 物件的形式儲存的。使用下面的 using 簡化程式碼:
using std::string;
using std::unordered_multimap;
using Name = std::pair<string, string>;
using Phone = std::tuple<string, string, string>;
電話號碼可以用三個整數來表示,但這樣組合起來更像編碼而不是號碼。電話號碼中的每個元素都有固定個數的數位,有一些位數的組合是不允許的。如果想增加對號碼檢查的功能,顯然用 string 物件可以使號碼位數或區號的檢查更簡單。這裡並沒有包含這個功能,因為這會使本書的編碼量大大增加。

為了從 istream 物件讀取電話號碼,對 >> 運算子進行了過載。函數如下:
inline std::istream& operator>>(std::istream& in, Phones phone)
{
    string area_code {},exchange {}, number {};
    in >> std::ws >> area_code >> exchange >> number; phone = std::make_tuple(area_code, exchange, number); return in;
}
phone 是元組模板型別。這裡 make_tuple() 使用從 in 讀出的區域性變數的值生成了一個 phone 物件。

我們可以為 Name 物件做相同的事:
inline std::istream& operator>>(std::istream& in, Names& name)
{
    in >> std::ws >> name.first >> name.second;
    return in;
}
這裡會丟棄從 in 讀入的任何前置空格,然後讀出兩個 name 字串作為 pair 物件的成員。當然,我們也需要輸出功能。這裡定義了 operator<<() 來為 phone 物件提供輸出:
inline std::ostream& operator<<(std::ostream& out, const Phone& phone)
{
    std::string area_code {}, exchange {}, number {}; std::tie(area_codeA exchange, number) = phone;
    out << area_code <<" "<< exchange << " " << number; return out;
}
這裡使用函數模板 tie<>() 生了一個包含三個區域性變數參照的元組。然後為了將 phone 的成員變數的值儲存到區域性變數中,將 phone 賦值給 tie<>() 生成的元組,後面會輸出這些區域性變數。或者,使用函數模板 get<>() 來存取 phone 成員變數的值。這種方法顯然更好,因為能夠避免上面出現的 string 物件的副本,但這裡是為了展示 tie<>() 函數的使用。

為 Name 物件過載 << 是很簡單的:
inline std::ostream& operator<<(std::ostream& out, const Name& name)
{
    out << name.first << " " << name.second;
    return out;
}
所有的這些 I/O 函數都是內聯的,所以把它們放在叫作 Record_IO.h 的標頭檔案中。檔案的開頭是 #include 和 using:
#include <string>   //  For string class
#include <istream>  //  For istream class
#include <ostream>  //  For ostream class
#include <utility>  //  For pair type
#include <tuple>    //  For tuple type
using Name = std::pair <std::string, std::string>;
using Phone = std::tuple <std::string, std::string,std::string>;
這個程式會使用兩個關聯容器一其中一個以名稱作為鍵,另一個以電話號碼為鍵,所以它們都會包含相同的基本資訊,但它們的存取方式不同。當然有更有效率的方式來做這些,但這裡我們是為了嘗試 unordered_multimap 容器的使用。main() 中定義的容器如下:
unordered_multimap<Name, Phone, NameHash> by_name {8, NameHash()};
unordered_multimap<Phone, Name, PhoneHash> by_number {8, PhoneHash()};
這裡並沒有為 pair 或 tuple 物件提供預設的雜湊能力,所以我們不得不自己定義它們。 這裡它們分別是 NameHash 和 PhoneHash 型別的函數物件。建構函式中雜湊函數物件之前的引數是格子的個數,所以這裡的格子數是指定的。它們等於系統的預設值。

把這兩個雜湊函數型別的定義放在同一個標頭檔案中,名為 Hash_Function_Objects.h,檔案的開頭是下面這些程式碼:
#include <string>                                // For string class
#include <utility>                               // For pair type
#include <tuple>                                 // For tuple type

using Name = std::pair < std::string, std::string >;
using Phone = std::tuple < std::string, std::string, std::string >;
按如下方式定義 PhoneHash 型別:
// Hash a phone number
class PhoneHash
{
public:
    size_t operator()(const Phone& phone) const
    {
        return std::hash<std::string>()(std::get<0>(phone)+std::get<1>(phone)+std::get<2>(phone));
    }
};
通過用定義在 string 標頭檔案中的 hash<string>() 模板的特例化來處理電話號碼中三個元素串聯後的結果,對應地生成雜湊值。下面以相似的方式定義了 HashName 型別:
// Hash a name
class NameHash
{
public:
    size_t operator()(const Name& name) const
    {
        return std::hash<std::string>()(name.first + name.second);
    }
};
將輸出操作打包放在單獨的函數中:
void show_operations()
{
    std::cout << "Operations:n"<< "A: Add an element.n"<< "D: Delete elements.n"<< "F: Find elements.n"<< "L: List all elements.n"<< "Q: Quit the program.nn";
}
通過名稱或號碼列出所有的元素。我們定義了一個函數模板來處理這兩種可能:
template<typename Container>
void list_elements(const Container& container)
{
    for(const auto& element : container)
        std::cout << element.first << "  " << element.second << std::endl;
}
當函數被呼叫時,這個模板會從使用的引數中推斷出容器的型別。兩個容器包含的元素是 pair 物件。by_name 容器包含的是 pair<Name,Phone>S 象型別的元素,by_number 容器包含的是 pair<Phone,Name> 物件型別的元素。因為我們己經為 Name 和 Phone 型別過載了 operator<<(),迴圈體會為 pair 元素的成員變數型別自動選擇合適的函數輸出。把這個函數模板和 My_Templates.h 標頭檔案後面的模板放在完整的範例中。

通過名稱或號碼來查詢元素的過程本質上是相同的,所以我們也可以為它們定義一個函數模板:
template<typename Container>
auto find_elements(const Container& container)
{
    typename Container::key_type key {};
    std::cin >> key;
    auto pr = container.equal_range(key);
    return pr;
}
這段程式碼對應於 C++11 標準。返回型別是依賴容器型別的 pair<> 模板型別。這是因為返回的 pair 封裝的疊代器型別是特定的容器型別。這就意味著編譯器不能處理常式名之前的返回型別,因為容器的型別取決於函數引數的型別,這個型別稍後才用得到。

為了能夠使 C++11 編譯器可以確定返回型別,必須使用尾部返回型別語法。這會允許編譯器在處理完函數引數後再處理返回型別。注意,typename 關鍵字對模板型別的規範是至關重要的,它對於區域性變數 key 的規範也至關重要。容器中 key 的型別是由容器類的成員變數 key_type 指定的,所以 key 的型別規範會自動從容器中選擇正確的型別。如果需要和鍵關聯的物件的型別,它是由成員 Container:mapped_type 指定的,Container::value_type 指定了容器元素的型別。

C++14 標準為編譯器引入了推斷函數返回型別的能力,因而函數模板可以這樣寫:
template<typename Container>
auto find_elements(const Container& container) -> std::pair<typename Container::const_iterator, typename Container::const_iterator>
{
    typename Container::key_type key {};
    std::cin >> key;
    auto pr = container.equal_range(key);
    return pr;
}
這裡不需要尾部返回型別,因為編譯器可以從返回值 pr 中推斷出返回型別。

這個操作允許通過搜尋名稱或號碼來查詢元素,每種情況都會返回一個 pair 物件,它包含的疊代器定義了一種或另一種型別元素的範圍。我們可以另外定義一個函數模板來輸出這段元素:
template<typename T>
void list_range(const T& pr)
{
    if(pr.first != pr.second)
    {
        for(auto iter = pr.first; iter != pr.second; ++iter)
            std::cout << "  " << iter->first << "  " << iter->second << std::endl;
    }
    else
        std::cout << "No records found.n";
}
如果作為引數的 pair 物件的成員變數是相同的,那麼範圍為空,這種情況下我們只會輸出一條訊息。為 Name 和 Phone 型別實現的 << 運算子函數使這個模板可以正常使用。pair 成員變數的實際型別會自動選擇合適的 operatorr<< 函數。需要注意的是,在已編譯的程式碼中,這些模板不會減少程式碼量。它們只是提供了一種生成所使用函數的轉換機制,提供了如何使用模板的簡單演示。

main() 函數包含下面這些程式碼:
#include <iostream>                              // For standard streams
#include <cctype>                                 // For toupper()
#include <string>                                // For string class
#include <unordered_map>                         // For unordered_map container

#include "Record_IO.h"
#include "My_Templates.h"
#include "Hash_Function_Objects.h"

using std::string;
using std::unordered_multimap;
using Name = std::pair < string, string >;
using Phone = std::tuple < string, string, string >;

// Display command prompt
void show_operations()
{
    std::cout << "Operations:n"<< "A: Add an element.n"<< "D: Delete elements.n"<< "F: Find elements.n"<< "L: List all elements.n"<< "Q: Quit the program.nn";
}

int main()
{
    unordered_multimap<Name, Phone, NameHash> by_name {8, NameHash()};
    unordered_multimap<Phone, Name, PhoneHash> by_number {8, PhoneHash()};
    show_operations();

    char choice {};                                     // Operation selection
    Phone number {};                                    // Records a number
    Name name {};                                       // Records a name

    while(std::toupper(choice) != 'Q')                  // Go until you quit...
    {
        std::cout << "Enter a command: ";
        std::cin >> choice;
        switch(std::toupper(choice))
        {
            case 'A':                                         // Add a record
                std::cout << "Enter first & second names, area code, exchange, and number separated by spaces:n";
                std::cin >> name >> number;
                by_name.emplace(name, number);                  // Create in place...
                by_number.emplace(number, name);                // ...in both containers
                break;
            case 'D':                                         // Delete records
            {
                std::cout << "Enter a name: ";                  // Only find by name
                auto pr = find_elements(by_name);
                auto count = std::distance(pr.first, pr.second);  // Number of elements
                if(count == 1)
                {                                               // If there's just the one...
                    by_number.erase(pr.first->second);            // ...delete from numbers container
                    by_name.erase(pr.first);                      // ...delete from names container
                }
                else if(count > 1)
                { // There's more than one
                    std::cout << "There are " << count << " records for "<< pr.first->first << ". Delete all(Y or N)? ";
                    std::cin >> choice;
                    if(std::toupper(choice) == 'Y')
                    { // Erase records from by_number container first
                        for(auto iter = pr.first; iter != pr.second; ++iter)
                        {
                            by_number.erase(iter->second);
                        }
                        by_name.erase(pr.first, pr.second);         // Now delete from by_name
                    }
                }
            }
            break;

            case 'F':                                         // Find a record
                std::cout << "Find by name(Y or N)? ";
                std::cin >> choice;
                if(std::toupper(choice) == 'Y')
                {
                    std::cout << "Enter first name and second name: ";
                    list_range(find_elements(by_name));
                }
                else
                {
                    std::cout << "Enter area code, exchange, and number separated by spaces: ";
                    list_range(find_elements(by_number));
                }
                break;

            case 'L':                                         //List all records
                std::cout << "List by name(Y or N)? ";
                std::cin >> choice;
                if(std::toupper(choice) == 'Y')
                    list_elements(by_name);
                else
                    list_elements(by_number);
                break;
            case 'Q':
                break;

            default:
                std::cout << "Invalid command - try again.n";
        }
    }
}
這段程式碼很好理解。在輸入選項後,程式就會執行相應的操作,直到輸入 'q' 或 'Q'。 迴圈體是一條大的 switch 語句,用來選擇合適的操作。

新增元素只涉及每個容器元素的生成,by_name 使用的鍵/物件值和 by_number 使用的是相反的。

可以用 find_elements() 來刪除 by_name 容器的元素,為了保證兩個容器中內容的同步,需要刪除 by_number 容器的對應元素。為了能夠從 by_name 容器移除多個元素,需要用定義元素範圍的疊代器作為 erase() 的引數。如果所有元素的鍵相同,就可以以這個範圍內的第一個元素的鍵為引數來刪除它們,例如:
by_name.erase(pr.first->first); // Delete elements with the specified key
為了查詢操作,find_elements() 模板範例返回的 pair 必須直接傳給 list_range() 模板的範例。編譯器會自動保證生成合適的呼叫。最後,為了可以列出元素,必須用指定的鍵為引數來呼叫一個 list elements() 模板的範例,從而輸出元素。

由於程式執行結果很佔篇幅,各位讀者可自行執行程式檢視執行結果。