如何修改關聯式容器中鍵值對的鍵?

2020-07-16 10:05:22
通過前面的學習,讀者已經掌握了所有關聯式容器(包括 map、multimap、set 和 multiset)的特性和用法。其中需要指出的是,對於如何修改容器中某個鍵值對的鍵,所有關聯式容器可以採用同一種解決思路,即先刪除該鍵值對,然後再向容器中新增修改之後的新鍵值對。

那麼,是否可以不刪除目標鍵值對,而直接修改它的鍵呢?接下來就圍繞此問題,給讀者展開詳細的講解。

首先可以明確的是,map 和 multimap 容器只能採用“先刪除,再新增”的方式修改某個鍵值對的鍵。原因很簡單,C++ STL 標準中明確規定,map 和 multimap 容器用於儲存型別為 pair<const K, V> 的鍵值對。顯然,只要目標鍵值對儲存在當前容器中,鍵的值就無法被修改。

舉個例子:
map<int, int> mymap{ {1,10},{2,20} };
//map 容器的鍵為 const 型別,不能被修改
mymap.begin()->first = 100;

multimap<int, int> mymultimap{ {10,100},{20,200} };
//multimap 容器的鍵為 const 型別,同樣不能被修改
mymultimap.begin()->first = 100;
其中,第 3 行程式碼試圖直接將 mymap 容器中 {1,10} 的鍵改為 100,同樣第 7 行程式碼試圖直接將 mymultimap 容器中 {10,100} 的鍵改為 100,它們都是不能通過編譯的。

正如上面例子中演示的那樣,直接修改 map 或 multimap 容器中某個鍵值對的鍵是行不通的。但對於 set 或者 multiset 容器來說,卻是可行的。

和 map、multimap 不同,C++ STL 標準中並沒有用 const 限定 set 和 multiset 容器中儲存元素的型別。換句話說,對於 set<T> 或者 multiset<T> 型別的容器,其儲存元素的型別是 T 而不是 const T。

事實上,對 set 和 multiset 容器中的元素型別作 const 修飾,是違背常理的。舉個例子,假設我們使用 set 容器儲存多個學生資訊,如下是一個表示學生的類:
class student {
public:
    student(string name, int id, int age) :name(name), id(id), age(age) {
    }
    const int& getid() const {
        return id;
    }
    void setname(const string name){
        this->name = name;
    }
    string getname() const{
        return  name;
    }
    void setage(int age){
        this->age = age;
    }
    int getage() const{
        return  age;
    }
private:
    string name;
    int id;
    int age;
};
在建立 set 容器之前,我們還需要為其設計一個排序規則,這裡假定以每個學生的 id 做升序排序,其排序規則如下:
class cmp {
public:
    bool operator ()(const student &stua, const student &stub) {
        //按照字串的長度,做升序排序(即儲存的字串從短到長)
        return  stua.getid() < stub.getid();
    }
};
做完以上所有的準備工作後,就可以建立一個可儲存 student 物件的 set 容器了,比如:
set<student, cmp> myset{ {"zhangsan",10,20},{"lisi",20,21},{"wangwu",15,19} };
由此建立的 myset 容器中,儲存的資料依次為:

{"zhangsan",10,20}
{"wangwu",15,19}
{"lisi",20,21}

注意,set 容器中每個元素也可以看做是鍵和值相等的鍵值對,但對於這裡的 myset 容器來說,其實每個 student 物件的 id 才是真正的鍵,其它資訊(name 和 age)只不過是和 id 係結在一起而已。因此,在不破壞 myset 容器中元素的有序性的前提下(即不修改每個學生的 id),學生的其它資訊是應該允許修改的,但有一個前提,即 myset 容器中儲存的各個 student 物件不能被 const 修飾(這也是 set 容器中的元素型別不能被 const 修飾的原因)。

總之,set 和 multiset 容器的元素型別沒有用 const 修飾。所以從語法的角度分析,我們可以直接修改容器中元素的值,但一定不要修改元素的鍵。

例如,在已建立好的 myset 容器的基礎上,如下程式碼嘗試修改 myset 容器中某個學生的 name 名字:
set<student>::iterator iter = mymap.begin();
(*iter).setname("xiaoming");
注意,如果讀者執行程式碼會發現,它也是無法通過編譯的。

雖然 C++ STL 標準沒有用 const 修飾 set 或者 multiset 容器中元素的型別,但也做了其它工作來限制使用者修改容器的元素。例如上面程式碼中,*iter 會呼叫 operator*,其返回的是一個 const T& 型別元素。這意味著,C++ STL 標準不允許使用者借助迭代器來直接修改 set 或者 multiset 容器中的元素。

那麼,如何才能正確修改 set 或 multiset 容器中的元素呢?最直接的方式就是借助 const_cast 運算子,該運算子可以去掉指標或者參照的 const 限定符。

有關 const_cast 運算子的用法,由於不是本節重點,這裡不再做詳細講解,有興趣的讀者可自行查閱相關資料。

比如,我們只需要借助 const_cast 運算子對上面程式稍作修改,就可以執行成功:
set<student>::iterator iter = mymap.begin();
const_cast<student&>(*iter).setname("xiaoming");
由此,mymap 容器中的 {"zhangsan",10,20} 就變成了 {"xiaoming",10,20}。

再次強調,雖然使用 const_cast 能直接修改 set 或者 multiset 容器中的元素,但一定不要修改元素的鍵!如果要修改,只能採用“先刪除,再新增”的方式。另外,不要試圖以同樣的方式修改 map 或者 multimap 容器中鍵值對的鍵,這違反了 C++ STL 標準的規定。

總結

總的來說,map 和 multimap 容器中元素的鍵是無法直接修改的,但借助 const_cast,我們可以直接修改 set 和 multiset 容器中元素的非鍵部分。

為了加深讀者的理解,如下是和本節內容相關的完整程式,讀者可直接拷貝下來:
#include<iostream>
#include<set>
#include<string>
using namespace std;
class student {
public:
    student(string name, int id, int age) :name(name), id(id), age(age) {
    }
    const int& getid() const {
        return id;
    }
    void setname(const string name){
        this->name = name;
    }
    string getname() const{
        return  name;
    }
    void setage(int age){
        this->age = age;
    }
    int getage() const{
        return  age;
    }
    void display()const {
        cout << id << " " << name << " " << age << endl;
    }
private:
    string name;
    int id;
    int age;
};
//自定義 myset 容器的排序規則
class cmp {
public:
    bool operator ()(const student &stua, const student &stub) {
        //按照字串的長度,做升序排序(即儲存的字串從短到長)
        return  stua.getid() < stub.getid();
    }
};

int main() {
    set<student, cmp> mymap{ {"zhangsan",10,20},{"lisi",20,21},{"wangwu",15,19} };

    set<student>::iterator iter = mymap.begin();
    //直接將 {"zhangsan",10,20} 中的 "zhangsan" 修改為 "xiaoming"
    const_cast<student&>(*iter).setname("xiaoming");
   
    while (iter != mymap.end()) {
        (*iter).display();
        ++iter;
    }
    return 0;
}
程式執行結果為:

10 xiaoming 20
15 wangwu 19
20 lisi 21