1. 程式人生 > >怎樣為std::map的自定義key提供比較操作(一)

怎樣為std::map的自定義key提供比較操作(一)

  stl的關聯容器(map,set)的key一般要求提供 < 比較操作。假設我們有一個結構SomeKey:

struct SomeKey
{
    int a, b;
};

  要想以SomeKey作為std::map的key,需要為這個結構提供operator < 比較操作,比如:

// 實現1
bool operator < (const SomeKey& left, const SomeKey& right)
{
    if (left.a < right.a) // 主key
    {
        return true;
    }
    else
if (left.a == right.a && left.b < right.b) // 次key { return true; } else { return false; } }

  或者:

// 實現2
bool operator < (const SomeKey& left, const SomeKey& right)
{
    if (left.a != right.a) // 主key
    {
        return left.a < right.a;
    }

    if
(left.b != right.b) // 次key { return left.b < right.b; } return false; }

  這兩種實現方式是很常見的了,似乎也沒什麼好聊的。不過在專案中,我一次又一次地遇到錯誤的operator < 實現,著實讓人吃驚!比如:

// 實現3(錯誤)
bool operator < (const SomeKey& left, const SomeKey& right)
{
    if (left.a < right.a) // 主key
    {
        return
true; } if (left.b < right.b) // 次key { return true; } return false; }

  或者

// 實現4(錯誤)
bool operator < (const SomeKey& left, const SomeKey& right)
{
    return (left.a < right.a || left.b < right.b);
}

  這樣的實現將導致荒謬的結果!比如有兩個SomeKey物件:x {10, 20} 和 y {5, 30},採用“實現3”或“實現4”來比較 x 和 y 的大小,結果取決於比較的時候誰在前面,也就是說,如果這樣比較:x < y,結果為真;而這樣:y < x,結果也為真!似乎作者並沒有理解 “ < ” 比較操作的含義。這種錯誤的比較操作,會導致std::map表現出怪異的行為:

std::map<SomeKey, int> m;
m.insert({ { 10, 20 }, 1 });
m.insert({ { 5, 30 }, 2 });
auto it = m.find({ 5, 30 });
if (it == m.end())
{
    std::cout << "找不到{5, 30}" << std::endl;
}
else
{
    std::cout << "找到{5, 30}" << std::endl;
}

  上面的程式碼片段會輸出“找不到{5, 30}”。在向map表insert鍵值對時,是以被插入值(後稱目標值)和紅黑樹上的節點(後稱節點值)比較:

插入{5, 30}

  而在map表中find時,是以節點值和目標值進行比較:

這裡寫圖片描述

  因為{10, 20} < {5, 30},所以在{10, 20}的右子樹上查詢,自然就找不到了。再向map表中插入一個節點,行為就更怪異了:

m.insert({ { 8, 25 }, 3 });
it = m.find({ 5, 30 });
if (it == m.end())
{
    std::cout << "找不到{5, 30}" << std::endl;
}
else
{
    std::cout << "找到{5, 30}" << std::endl;
}

  這次將輸出“找到{5, 30}”。因為{8, 25} < {10, 20},繼續{8, 25} < {5, 30},最終{8, 25}插入到map表的最左子節點,破壞了紅黑樹的平衡,右旋後,整棵樹變成:

這裡寫圖片描述

  然後在表中find {5, 30},首先找到第一個不小於{5, 30}的節點,因為第一個節點值{5, 30} 不小於目標值{5, 30},而目標值{5, 30}也不小於該節點值{5, 30},於是就找到了目標值。這種怪異行為導致的bug是比較隱祕的:你在表中找一個值,時而找得到,時而又找不到。一般的產品程式碼中,map表中插入的物件數量都不在少數,你討厭對大量資料的插入進行測試,你不會認為你的operator <區區幾行程式碼會有bug,你又不可能懷疑map有什麼問題,於是你很可能最終會將之歸結於靈異現象。

  在產品程式碼中,結構體可能會更復雜,類似的operator < 錯誤實現也不少見。


  本文以一個小學生都能理解的例子來聊聊正確的operator <實現應該是怎樣的。我們來比較兩個數字的大小(為便於討論,兩個數都是兩位的十進位制數):

  假如有:x = 36,y = 49;則 x < y 為真;
  假如有:x = 36,y = 29;則 x < y 不為真;
  假如有:x = 36,y = 39;則 x < y 為真;
  假如有:x = 36,y = 32;則 x < y 不為真;
  假如有:x = 36,y = 36;則 x < y 不為真;

  我們是怎麼正確比較出各種情況下 x 和 y 的大小的?實在是很簡單:首先比較高位數(十位數),誰的高位數小,誰就小;誰的高位數大,誰就大;高位數相等的話,那就以相同的方式再比較低位數(個位數)。為什麼要先比較高位數,因為高位數的“權”大啊!

  類似的,比較SomeKey的時候,哪個欄位先來,哪個欄位就是主key,SomeKey中,a是主key,b是次key,即 a 對於SomeKey物件“大小”的貢獻占主導地位,如果 a 能決出勝負來,b 就不用比了,只有 a 不能決出勝負時(相等),才繼續比較 b,無論怎麼比較,結果要是穩定的!

  “實現3”和“實現4”要這樣修改一下才行:

bool operator < (const SomeKey& left, const SomeKey& right)
{
    if (left.a < right.a) // 主key
    {
        return true; // 主key小,就小
    }

    if (left.a > right.a) // 主key
    {
        return false; // 主key大,就大
    }

    return left.b < right.b; // 主key相等,再比較次key
}

  說起來,本文其實不值一提!