【UE4 C++ 基礎知識】<6> 容器——TMap


概述

  • TMap主要由兩個類型定義(一個鍵類型和一個值類型),以關聯對的形式存儲在映射中。

  • 將數據存儲為鍵值對(TPair<KeyType, ValueType>),只將鍵用於存儲和獲取

  • 映射有兩種類型:TMapTMultiMap

    • TMap 中的鍵是唯一的
    • TMultiMap 可存儲多個相同的鍵
  • TMap 也是值類型,支持通常的復制、賦值和析構函數運算,以及它的元素的強所有權。在映射被銷毀時,它的元素都會被銷毀。鍵和值也必須為值類型。

  • TMap 是散列容器,這意味着鍵類型必須支持 GetTypeHash 函數,並提供 運算符== 來比較各個鍵是否等值

  • TMap 也可使用任選分配器來控制內存分配行為。但不同於 TArray,這些是集合分配器,而不是 FHeapAllocator 和 TInlineAllocator 之類的標准UE4分配器。集合分配器(TSetAllocator類)定義映射應使用的散列桶數量,以及應使用哪個標准UE4分配器來存儲散列和元素。

  • KeyFuncs 是最后一個 TMap 模板參數,該參數告知映射如何從元素類型獲取鍵,如何比較兩個鍵是否相等,以及如何對鍵進行散列計算。這些參數有默認值,它們只會返回對鍵的引用,使用 運算符== 確定相等性,並調用非成員 GetTypeHash 函數進行散列計算。如果您的鍵類型支持這些函數,可使用它作為映射鍵,不需要提供自定義 KeyFuncs。

  • 與 TArray 不同的是,內存中 TMap 元素的相對排序既不可靠也不穩定,對這些元素進行迭代很可能會使它們返回的順序和它們添加的順序有所不同。這些元素也不太可能在內存中連續排列。映射的支持數據結構是稀疏數組,這種數組可有效支持元素之間的空位。當元素從映射中被移除時,稀疏數組中就會出現空位。將新的元素添加到數組可填補這些空位。但是,即便 TMap 不會打亂元素來填補空位,指向映射元素的指針仍然可能失效,因為如果存儲器被填滿,又添加了新的元素,整個存儲可能會重新分配。

創建

TMap<int32, FString> FruitMap;  //空TMap,此時尚未分配內存

添加元素

  • Add

    • 元素按插入順序排列,但不保證這些元素在內存中實際保留此排序
    • 各個鍵都必定是唯一。如果嘗試添加重復鍵,將替換原來的鍵值
    • Add 函數可接受不帶值的鍵。調用此重載后的 Add 時,值將被默認構建
    FruitMap.Add(5, TEXT("Banana"));
    FruitMap.Add(2, TEXT("Grapefruit"));
    FruitMap.Add(7, TEXT("Pineapple"));
    // FruitMap == [
    //  { Key:5, Value:"Banana"     },
    //  { Key:2, Value:"Grapefruit" },
    //  { Key:7, Value:"Pineapple"  } ]
    
    FruitMap.Add(2, TEXT("Pear"));
    // FruitMap == [
    //  { Key:5, Value:"Banana"    },
    //  { Key:2, Value:"Pear"      },
    //  { Key:7, Value:"Pineapple" }
    // ]
    
    FruitMap.Add(4);
    // FruitMap == [
    //  { Key:5, Value:"Banana"    },
    //  { Key:2, Value:"Pear"      },
    //  { Key:7, Value:"Pineapple" },
    //  { Key:4, Value:""          }
    // ]
    
  • Emplace 可以代替 Add,防止插入映射時創建臨時文件

    FruitMap.Emplace(3, TEXT("Orange"));
    // FruitMap == [
    //  { Key:5, Value:"Banana"    },
    //  { Key:2, Value:"Pear"      },
    //  { Key:7, Value:"Pineapple" },
    //  { Key:4, Value:""          },
    //  { Key:3, Value:"Orange"    }
    // ]
    
  • Append 函數合並映射,將一個映射的所有元素移至另一個映射,源映射的相同鍵會替代目標映射中的鍵

    TMap<int32, FString> FruitMap2;
    FruitMap2.Emplace(4, TEXT("Kiwi"));
    FruitMap2.Emplace(9, TEXT("Melon"));
    FruitMap2.Emplace(5, TEXT("Mango"));
    FruitMap.Append(FruitMap2);
    // FruitMap == [
    //  { Key:5, Value:"Mango"     },
    //  { Key:2, Value:"Pear"      },
    //  { Key:7, Value:"Pineapple" },
    //  { Key:4, Value:"Kiwi"      },
    //  { Key:3, Value:"Orange"    },
    //  { Key:9, Value:"Melon"     }
    // ]
    // FruitMap2 is now empty.
    

迭代

  • 范圍 - for

    for (auto& Elem :FruitMap)
    {
        FPlatformMisc::LocalPrint( *FString::Printf(TEXT("(%d, \"%s\")\n"), Elem.Key, *Elem.Value)  );
    }
    // Output:
    // (5, "Mango")
    // (2, "Pear")
    // (7, "Pineapple")
    // (4, "Kiwi")
    // (3, "Orange")
    // (9, "Melon")
    
  • 迭代器

    • CreateIterator 返回擁有讀寫訪問權限的迭代器,
    • CreateConstIterator 返回擁有只讀訪問權限的迭代器
    for (auto It = FruitMap.CreateConstIterator(); It; ++It)
    {
        FPlatformMisc::LocalPrint(
            *FString::Printf( TEXT("(%d, \"%s\")\n"),
                It.Key(),   // same as It->Key
                *It.Value() // same as *It->Value
            ) );
    }
    

查詢

  • Num 函數查詢映射中保存的元素數量

    int32 Count = FruitMap.Num(); // Count == 6
    
  • Contains 函數查詢是否包含特定鍵

    bool bHas7 = FruitMap.Contains(7); // bHas7 == true
    bool bHas8 = FruitMap.Contains(8); // bHas8 == false
    
  • [] 運算符將鍵用作索引查找相應值

    • 使用非常量映射執行該操作將返回非常量引用,使用常量映射將返回常量引用。
    • 在使用 運算符[] 前,應檢查映射中是否存在該鍵。如果映射中鍵不存在,將觸發斷言。
    FString Val7 = FruitMap[7]; // Val7 == "Pineapple"
    FString Val8 = FruitMap[8]; // Assert!
    
  • Find 函數進行鍵查找

    • 如果映射包含該鍵,Find 將返回指向元素數值的指針;如果映射不包含該鍵,則返回null。
    • 在常量映射上調用 Find,返回的指針也將為常量。
    FString* Ptr7 = FruitMap.Find(7); // *Ptr7 == "Pineapple"
    FString* Ptr8 = FruitMap.Find(8); //  Ptr8 == nullptr
    
  • FindOrAdd 將返回對與給定鍵關聯的值的引用。

    • 如果映射中不存在該鍵,FindOrAdd 將返回新創建的元素(使用給定鍵和默認構建值),該元素也會被添加到映射。
    • FindOrAdd 可修改映射,因此僅適用於非常量映射。
    FString& Ref7 = FruitMap.FindOrAdd(7);
    // Ref7     == "Pineapple"
    // FruitMap == [
    //  { Key:5, Value:"Mango"     },
    //  { Key:2, Value:"Pear"      },
    //  { Key:7, Value:"Pineapple" },
    //  { Key:4, Value:"Kiwi"      },
    //  { Key:3, Value:"Orange"    },
    //  { Key:9, Value:"Melon"     }
    // ]
    
    FString& Ref8 = FruitMap.FindOrAdd(8);
    // Ref8     == ""
    // FruitMap == [
    //  { Key:5, Value:"Mango"     },
    //  { Key:2, Value:"Pear"      },
    //  { Key:7, Value:"Pineapple" },
    //  { Key:4, Value:"Kiwi"      },
    //  { Key:3, Value:"Orange"    },
    //  { Key:9, Value:"Melon"     },
    //  { Key:8, Value:""          }
    // ]
    //如已發生重新分配,此處的 Ref7 引用可能會因 FruitMap.FindOrAdd(8) 的調用而無效化。
    
  • FindRef 會返回與給定鍵關聯的值副本

    • 若映射中未找到給定鍵,則返回默認構建值。
    • FindRef 不會創建新元素,因此既可用於常量映射,也可用於非常量映射。
    FString Val7 = FruitMap.FindRef(7);
    FString Val6 = FruitMap.FindRef(6);
    // Val7     == "Pineapple"
    // Val6     == ""
    // FruitMap == [
    //  { Key:5, Value:"Mango"     },
    //  { Key:2, Value:"Pear"      },
    //  { Key:7, Value:"Pineapple" },
    //  { Key:4, Value:"Kiwi"      },
    //  { Key:3, Value:"Orange"    },
    //  { Key:9, Value:"Melon"     },
    //  { Key:8, Value:""          }
    // ]
    
  • FindKey 函數執行逆向查找

    • 返回指向與所提供值配對的第一個鍵的指針。搜索映射中不存在的值將返回空鍵。
    • 按值查找比按鍵查找慢(線性時間)。這是因為映射按鍵排序,而非按值排序。
    • 如果映射有多個具有相同值的鍵,FindKey 可返回其中任一鍵。
    const int32* KeyMangoPtr   = FruitMap.FindKey(TEXT("Mango"));   // *KeyMangoPtr   == 5
    const int32* KeyKumquatPtr = FruitMap.FindKey(TEXT("Kumquat")); //  KeyKumquatPtr == nullptr
    
  • GenerateKeyArrayGenerateValueArray 分別使用所有鍵和值的副本來填充 TArray。

    • 在這兩種情況下,都會在填充前清空所傳遞的數組,因此產生的元素數量始終等於映射中的元素數量
    TArray<int32>   FruitKeys;
    TArray<FString> FruitValues;
    FruitKeys.Add(999);
    FruitKeys.Add(123);
    FruitMap.GenerateKeyArray  (FruitKeys);
    FruitMap.GenerateValueArray(FruitValues);
    // FruitKeys   == [ 5,2,7,4,3,9,8 ]
    // FruitValues == [ "Mango","Pear","Pineapple","Kiwi","Orange","Melon","" ]
    

移除

  • Remove 函數提供要移除元素的鍵。

    • 返回值是被移除元素的數量。
    • 如果映射不包含與鍵匹配的元素,則返回值可為零。
    • 移除元素將在數據結構(在Visual Studio的觀察窗口中可視化映射時可看到)中留下空位
    FruitMap.Remove(8);
    // FruitMap == [
    //  { Key:5, Value:"Mango"     },
    //  { Key:2, Value:"Pear"      },
    //  { Key:7, Value:"Pineapple" },
    //  { Key:4, Value:"Kiwi"      },
    //  { Key:3, Value:"Orange"    },
    //  { Key:9, Value:"Melon"     }
    // ]
    
  • FindAndRemoveChecked 函數可用於從映射移除元素並返回其值。

    • 名稱中的checked部分意味着將檢查鍵是否存在,如果不存在,則出現斷言:
    FString Removed7 = FruitMap.FindAndRemoveChecked(7);
    // Removed7 == "Pineapple"
    // FruitMap == [
    //  { Key:5, Value:"Mango"  },
    //  { Key:2, Value:"Pear"   },
    //  { Key:4, Value:"Kiwi"   },
    //  { Key:3, Value:"Orange" },
    //  { Key:9, Value:"Melon"  }
    // ]
    
    FString Removed8 = FruitMap.FindAndRemoveChecked(8);
    // Assert!
    
  • RemoveAndCopyValue 函數的作用與 Remove 相似,不同點是會將已移除元素的值復制到引用參數。如果映射中不存在指定的鍵,則輸出參數將保持不變,函數將返回 false。

    FString Removed;
    bool bFound2 = FruitMap.RemoveAndCopyValue(2, Removed);
    // bFound2  == true
    // Removed  == "Pear"
    // FruitMap == [
    //  { Key:5, Value:"Mango"  },
    //  { Key:4, Value:"Kiwi"   },
    //  { Key:3, Value:"Orange" },
    //  { Key:9, Value:"Melon"  } ]
    
    bool bFound8 = FruitMap.RemoveAndCopyValue(8, Removed);
    // bFound8  == false
    // Removed  == "Pear", i.e. unchanged
    // FruitMap == [
    //  { Key:5, Value:"Mango"  },
    //  { Key:4, Value:"Kiwi"   },
    //  { Key:3, Value:"Orange" },
    //  { Key:9, Value:"Melon"  } ]
    
  • EmptyReset 函數可將映射中的所有元素移除。

    • Empty 可采用參數指示映射中保留的slack量
    • Reset 則是盡可能多地留出slack量。
    TMap<int32, FString> FruitMapCopy = FruitMap;
    // FruitMapCopy == [
    //  { Key:5, Value:"Mango"  },
    //  { Key:4, Value:"Kiwi"   },
    //  { Key:3, Value:"Orange" },
    //  { Key:9, Value:"Melon"  }
    // ]
    
    FruitMapCopy.Empty();       // We could also have called Reset() here.
    // FruitMapCopy == []
    

排序

TMap 可以進行排序。排序后,迭代映射會以排序的順序顯示元素,但下次修改映射時,排序可能會發生變化。排序是不穩定的,因此等值元素在MultiMap中可能以任何順序出現。

  • 使用 KeySortValueSort 函數可分別按鍵和值進行排序。兩個函數均使用二元謂詞來進行排序:

    FruitMap.KeySort([](int32 A, int32 B) {
        return A > B; // sort keys in reverse
    });
    // FruitMap == [
    //  { Key:9, Value:"Melon"  },
    //  { Key:5, Value:"Mango"  },
    //  { Key:4, Value:"Kiwi"   },
    //  { Key:3, Value:"Orange" }
    // ]
    
    FruitMap.ValueSort([](const FString& A, const FString& B) {
        return A.Len() < B.Len(); // sort strings by length
    });
    // FruitMap == [
    //  { Key:4, Value:"Kiwi"   },
    //  { Key:5, Value:"Mango"  },
    //  { Key:9, Value:"Melon"  },
    //  { Key:3, Value:"Orange" }
    // ]
    

運算符

  • 和 TArray 一樣,TMap 是常規值類型,可通過標准復制構造函數或賦值運算符進行復制。因為映射嚴格擁有其元素,復制映射的操作是深層的,所以新的映射將擁有其自己的元素副本。

    TMap<int32, FString> NewMap = FruitMap;
    NewMap[5] = "Apple";
    NewMap.Remove(3);
    // FruitMap == [
    //  { Key:4, Value:"Kiwi"   },
    //  { Key:5, Value:"Mango"  },
    //  { Key:9, Value:"Melon"  },
    //  { Key:3, Value:"Orange" }
    // ]
    // NewMap == [
    //  { Key:4, Value:"Kiwi"  },
    //  { Key:5, Value:"Apple" },
    //  { Key:9, Value:"Melon" }
    // ]
    
  • MoveTemp 函數可調用移動語義。在移動后,源映射必定為空

    FruitMap = MoveTemp(NewMap);
    // FruitMap == [
    //  { Key:4, Value:"Kiwi"  },
    //  { Key:5, Value:"Apple" },
    //  { Key:9, Value:"Melon" }
    // ]
    // NewMap == []
    

Slack

簡介

  • Slack是不包含元素的已分配內存。調用 Reserve 可分配內存,無需添加元素;
  • 通過非零slack參數調用 Reset 或 Empty 可移除元素,無需將其使用的內存取消分配。
  • Slack優化了將新元素添加到映射的過程,因為可以使用預先分配的內存,而不必分配新內存。
  • 它在移除元素時也十分實用,因為系統不需要將內存取消分配。在清空希望用相同或更少的元素立即重新填充的映射時,此方法尤其有效。
  • TMap 不像 TArray 中的 Max 函數那樣可以檢查預分配元素的數量。

函數

  • Reset

    FruitMap.Reset();
    // FruitMap == [<invalid>, <invalid>, <invalid>]
    // 內存不釋放
    
  • Reserve 函數預先分配映射

    FruitMap.Reserve(10);
    for (int32 i = 0; i < 10; ++i)
    {
        FruitMap.Add(i, FString::Printf(TEXT("Fruit%d"), i));
    }
    // FruitMap == [
    //  { Key:9, Value:"Fruit9" },
    //  { Key:8, Value:"Fruit8" },
    //  ...
    //  { Key:1, Value:"Fruit1" },
    //  { Key:0, Value:"Fruit0" }
    // ]
    
    
  • 使用 CollapseShrink 函數可移除 TMap 中的全部slack。

    • Shrink 將從容器的末端移除所有slack,但這會在中間或開始處留下空白元素。
    for (int32 i = 0; i < 10; i += 2)
    {
        FruitMap.Remove(i);
    }
    // FruitMap == [
    //  { Key:9, Value:"Fruit9" },
    //  <invalid>,
    //  { Key:7, Value:"Fruit7" },
    //  <invalid>,
    //  { Key:5, Value:"Fruit5" },
    //  <invalid>,
    //  { Key:3, Value:"Fruit3" },
    //  <invalid>,
    //  { Key:1, Value:"Fruit1" },
    //  <invalid>
    // ]
    FruitMap.Shrink();
    // FruitMap == [
    //  { Key:9, Value:"Fruit9" },
    //  <invalid>,
    //  { Key:7, Value:"Fruit7" },
    //  <invalid>,
    //  { Key:5, Value:"Fruit5" },
    //  <invalid>,
    //  { Key:3, Value:"Fruit3" },
    //  <invalid>,
    //  { Key:1, Value:"Fruit1" }
    // ]
    //代碼中,Shrink 只刪除了一個無效元素,因為末端只有一個空元素
    
  • Compact 函數將空白空間組合在一起,為調用 Shrink 做好准備

    FruitMap.Compact();
    // FruitMap == [
    //  { Key:9, Value:"Fruit9" },
    //  { Key:7, Value:"Fruit7" },
    //  { Key:5, Value:"Fruit5" },
    //  { Key:3, Value:"Fruit3" },
    //  { Key:1, Value:"Fruit1" },
    //  <invalid>,
    //  <invalid>,
    //  <invalid>,
    //  <invalid>
    // ]
    FruitMap.Shrink();
    // FruitMap == [
    //  { Key:9, Value:"Fruit9" },
    //  { Key:7, Value:"Fruit7" },
    //  { Key:5, Value:"Fruit5" },
    //  { Key:3, Value:"Fruit3" },
    //  { Key:1, Value:"Fruit1" }
    // ]
    

KeyFuncs

只要類型具有 運算符== 和非成員 GetTypeHash 重載,就可用作 TMap 的KeyType,不需要任何更改。但是,可能需要將類型用作鍵,而不重載這些函數。在這些情況下,可對 KeyFuncs 進行自定義。為鍵類型創建 KeyFuncs,必須定義兩個typedef和三個靜態函數,如下所示:

  • KeyInitType —— 用於傳遞鍵的類型。
  • ElementInitType —— 用於傳遞元素的類型。
  • KeyInitType GetSetKey(ElementInitType Element)——返回元素的鍵。
  • bool Matches(KeyInitType A, KeyInitType B) —— 如果 A 和 B 等值將返回 true,否則返回 false
  • uint32 GetKeyHash(KeyInitType Key) —— 返回 Key 的散列值。

KeyInitType 和 ElementInitType 是鍵類型和值類型的常規傳遞約定的typedef。它們通常為淺顯類型的一個值,和非淺顯類型的一個常量引用。請記住,映射的元素類型是 TPair

struct FMyStruct
{
    // String which identifies our key
    FString UniqueID;

    // Some state which doesn't affect struct identity
    float SomeFloat;

    explicit FMyStruct(float InFloat)
        :UniqueID (FGuid::NewGuid().ToString())
        , SomeFloat(InFloat)
    {
    }
};
template <typename ValueType>
struct TMyStructMapKeyFuncs :
    BaseKeyFuncs<
        TPair<FMyStruct, ValueType>,
        FString
    >
{
private:
    typedef BaseKeyFuncs<
        TPair<FMyStruct, ValueType>,
        FString
    > Super;

public:
    typedef typename Super::ElementInitType ElementInitType;
    typedef typename Super::KeyInitType     KeyInitType;

    static KeyInitType GetSetKey(ElementInitType Element)
    {
        return Element.Key.UniqueID;
    }

    static bool Matches(KeyInitType A, KeyInitType B)
    {
        return A.Compare(B, ESearchCase::CaseSensitive) == 0;
    }

    static uint32 GetKeyHash(KeyInitType Key)
    {
        return FCrc::StrCrc32(*Key);
    }
};

FMyStruct 具有唯一標識符,以及一些與身份無關的其他數據。GetTypeHash 和 運算符== 不適用於此,因為 運算符== 為實現通用目的不應忽略任何類型的數據,但同時又需要如此才能與 GetTypeHash 的行為保持一致,后者只關注 UniqueID 字段。以下步驟有助於為 FMyStruct 創建自定義 KeyFuncs

  1. 首先,繼承 BaseKeyFuncs,因為它可以幫助定義某些類型,包括 KeyInitType 和 ElementInitType

    BaseKeyFuncs 使用兩個模板參數:映射的元素類型和鍵類型。和所有映射一樣,元素類型是 TPair,使用 FMyStruct 作為其 KeyTypeTMyStructMapKeyFuncs 的模板參數作為其 ValueType。將備用 KeyFuncs 用作模板,可為每個映射指定 ValueType,因此每次要在 FMyStruct 上創建鍵控 TMap 時不必定義新的 KeyFuncs。第二個 BaseKeyFuncs 參數是鍵類型,不要與元素存儲的鍵區(TPair 的 KeyType)混淆。因為此映射應使用 UniqueID(來自 FMyStruct)作為鍵,所以此處使用 FString

  2. 然后,定義三個必需的 KeyFuncs 靜態函數。第一個是 GetSetKey,該函數返回給定元素類型的鍵。由於元素類型是 TPair,而鍵是 UniqueID,所以該函數可直接返回 UniqueID

    第二個靜態函數是 Matches,該函數接受兩個元素的鍵(由 GetSetKey 獲取),然后比較它們是否相等。在 FString 中,標准的等效測試(運算符==)不區分大小寫;要替換為區分大小寫的搜索,請用相應的大小寫對比選項使用 Compare 函數。

  3. 最后,GetKeyHash 靜態函數接受提取的鍵並返回其散列值。由於 Matches 函數區分大小寫,GetKeyHash 也必須區分大小寫。區分大小寫的 FCrc 函數將計算鍵字符串的散列值。

  4. 現在結構已滿足 TMap 要求的行為,可創建它的實例。

TMap<
        FMyStruct,
        int32,
        FDefaultSetAllocator,
        TMyStructMapKeyFuncs<int32>
    > MyMapToInt32;

    // Add some elements
    MyMapToInt32.Add(FMyStruct(3.14f), 5);
    MyMapToInt32.Add(FMyStruct(1.23f), 2);

    // MyMapToInt32 == [
    //  {
    //      Key:{
    //          UniqueID:"D06AABBA466CAA4EB62D2F97936274E4",
    //          SomeFloat:3.14f
    //      },
    //      Value:5
    //  },
    //  {
    //      Key:{
    //          UniqueID:"0661218447650259FD4E33AD6C9C5DCB",
    //          SomeFloat:1.23f
    //      },
    //      Value:5
    //  }
    // ]

在自行設置KeyFuncs時,要注意 TMap 假設兩個項目使用 Matches 比較的結果相等,則它們會從 GetKeyHash 返回相同的值。此外,如果對現有映射元素的鍵進行的修改將會改變來自這兩個函數中任一個的結果,那么系統會將這種修改視作未定義的行為,因為這會使映射的內部散列失效。這些規則也適用於使用默認 KeyFuncs 時 運算符== 和 GetKeyHash 的重載。

其他

CountBytes 和 GetAllocatedSize 函數用於估計內部數組的當前內存使用情況。CountBytes 接受 Farchive 參數,而 GetAllocatedSize 則不會。這些函數常用於統計報告。

Dump 函數接受 FOutputDevice,並寫出關於映射內容的實現信息。此函數常用於調試。

參考

TMap


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM