After TArray
, the most commonly used container in Unreal Engine 4 (UE4) is TMap
. TMap
is similar to TSet
in that its structure is based on hashing keys. However, unlike TSet
, this container stores data as key-value pairs (TPair<KeyType, ValueType>
), using keys only for storage and retrieval.
There are two types of map: TMap
and TMultiMap
. The difference between these two is that TMap
keys are unique, while TMultiMap
supports storing multiple, identical keys. When adding a new key-value pair to a TMap
with a key that matches an existing pair, the new pair will replace the old one. In a TMultiMap
, the container will store both the new pair and the old.
TMap
In a TMap
, key-value pairs are treated as the element type of the map as if each pair were an individual object. In this document, element means a key-value pair, while individual components are referred to as the element's key or the element's value. The element type is actually a TPair<KeyType, ElementType>
, though it should be rare to need to refer to the TPair
type directly.
Like TArray
, TMap
is a homogeneous container, meaning that all of its elements are strictly the same type. TMap
is also a value type, and supports the usual copy, assignment, and destructor operations, as well as strong ownership of its elements, which will be destroyed when the map is destroyed. The key and value must also be value types.
TMap
is a hashing container, which means that the key type must support the GetTypeHash
function and provide an operator==
for comparing keys for equality. Hashing is covered in more detail later.
TMap
can also take an optional allocator to control the memory allocation behavior. However, unlike TArray
, these are set allocators rather than the standard UE4 allocators like FHeapAllocator
and TInlineAllocator
. Set allocators, (class TSetAllocator
), define how many hash buckets the map should use and which standard UE4 allocators should be used for hash and element storage.
The final TMap
template parameter is KeyFuncs
, which tells the map how to retrieve the key from the element type, how to compare two keys for equality and how to hash the key. These have defaults which will just return a reference to the key, use operator==
for equality, and call the non-member GetTypeHash
function for hashing. If your key type supports these functions, you can use it as a map key without supplying a custom KeyFuncs
.
Unlike TArray
, the relative order of TMap
elements in memory is not reliable or stable, and iterating over the elements is likely to return them in a different order from the order in which they were added. Elements are also unlikely to be laid out contiguously in memory. The backing data structure of a map is a sparse array, which is an array that efficiently supports gaps between its elements. As elements are removed from the map, gaps in the sparse array will appear. Adding new elements into the array can then fill these gaps. However, even though TMap
doesn't shuffle elements to fill gaps, pointers to map elements may still be invalidated, as the entire storage can be reallocated when it is full and new elements are added.
Creating and Filling a Map
You can create a TMap
like this:
TMap<int32, FString> FruitMap;
FruitMap
is now an empty TMap
of strings that are identified by integer keys. We have specified neither an allocator nor a KeyFuncs
, so the map will do standard heap allocation and compare the key (of type int32
) using operator==
and hash it using GetTypeHash
. No memory has been allocated at this point.
The standard way to populate a map is to call the Add
function with a key and a value:
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" }
// ]
This is not a TMultiMap
, so keys are guaranteed to be unique. The following is the result of attempting to add a duplicate key:
FruitMap.Add(2, TEXT("Pear"));
// FruitMap == [
// { Key: 5, Value: "Banana" },
// { Key: 2, Value: "Pear" },
// { Key: 7, Value: "Pineapple" }
// ]
The map still contains three elements, but the previous "Grapefruit" value with a key of 2 has been replaced with "Pear".
The Add
function can accept a key without a value. When this overloaded Add
is called, the value will be default-constructed:
FruitMap.Add(4);
// FruitMap == [
// { Key: 5, Value: "Banana" },
// { Key: 2, Value: "Pear" },
// { Key: 7, Value: "Pineapple" },
// { Key: 4, Value: "" }
// ]
Like TArray
, we can also use Emplace
instead of Add
to avoid the creation of temporaries when inserting into the map:
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" }
// ]
Here, the key and value are passed directly to their respective type constructors. While this isn't meaningful for the int32
key, it does avoid the creation of a temporary FString
for the value. Unlike TArray
, it's only possible to emplace elements into a map with single-argument constructors.
You can also merge two maps with the Append
function, which will move all elements from one map into the other:
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.
In the above example, the resulting map is equivalent to using Add
or Emplace
to add each element of FruitMap2
individually, emptying FruitMap2
when the process is complete. This means that any element from FruitMap2
that shares its key with an element already in FruitMap
will replace that element.
If you mark the TMap
with the UPROPERTY
macro and one of the "editable" keywords (EditAnywhere
, EditDefaultsOnly
, or EditInstanceOnly
), you can add and edit elements in the Editor.
UPROPERTY(Category = MapsAndSets, EditAnywhere)
TMap<int32, FString> FruitMap;
Iteration
Iteration over TMaps
is similar to TArrays
. You can use C++'s ranged-for feature, remembering that the element type is a TPair
:
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")
You can also create iterators with the CreateIterator
and CreateConstIterators
functions. CreateIterator
will return an iterator with read-write access, while CreateConstIterator
returns a read-only iterator. In either case, you can use the Key
and Value
functions of these iterators to examine the elements. Printing the contents of our example "fruit" map using iterators would look like this:
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
)
);
}
Queries
To find out how many elements are currently in the map, call the Num
function.
int32 Count = FruitMap.Num();
// Count == 6
In order to determine whether or not a map contains a specific key, call the Contains
function, as follows:
bool bHas7 = FruitMap.Contains(7);
bool bHas8 = FruitMap.Contains(8);
// bHas7 == true
// bHas8 == false
If you know that your map contains a certain key, you can look up the corresponding value with operator[]
, using the key as the index. Doing this with a non-const map will return a non-const reference, while a const map will return a const reference.
operator[]
. If the map does not contain the key, it will assert. FString Val7 = FruitMap[7];
// Val7 == "Pineapple"
FString Val8 = FruitMap[8];
// Assert!
If you are unsure whether or not your map contains a key, you could check with the Contains
function, and then use operator[]
. However, this is suboptimal, since a successful retrieval involves two lookups on the same key. The Find
function combines these behaviors with a single lookup. Find
returns a pointer to the value of the element if the map contains the key, or a null pointer if it does not. Calling Find
on a const map will cause the pointer it returns to be const as well.
FString* Ptr7 = FruitMap.Find(7);
FString* Ptr8 = FruitMap.Find(8);
// *Ptr7 == "Pineapple"
// Ptr8 == nullptr
Alternately, to ensure that you get a valid result from your query, you can use FindOrAdd
or FindRef
. FindOrAdd
will return a reference to the value associated with the key you provide. If the key was not in the map, FindOrAdd
will return a newly-created element, with your key and the default-contructed value, that it will also add to the map. Because it can potentially modify the map, FindOrAdd
is only available for non-const maps. FindRef
, despite its name, will return a copy of the value associated with your key, or a default-constructed value if your key is not found in the map. FindRef
does not create a new element, and thus is available for use with both const and non-const maps. Because FindOrAdd
and FindRef
succeed even when the key isn't found in the map, you can safely call them without the usual safety procedures like checking Contains
in advance, or null-checking the return value.
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: "" }
// ]
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: "" }
// ]
Because FindOrAdd
can add new entries to the map, as it does when initializing Ref8
in our example, previously-obtained pointers (from Find
) or references (from FindOrAdd
) could become invalid. This is a result of the addition operation allocating memory and moving existing data if the map's backend storage needs to expand to contain the new element. In the example above, Ref7
may be invalidated after Ref8
after the call to FindOrAdd(8)
.
The FindKey
function performs a reverse lookup, meaning that a supplied value is matched to a key, and returns a pointer to the first key that's paired to the provided value. Searches for a value that isn't present in the map will return a null key.
const int32* KeyMangoPtr = FruitMap.FindKey(TEXT("Mango"));
const int32* KeyKumquatPtr = FruitMap.FindKey(TEXT("Kumquat"));
// *KeyMangoPtr == 5
// KeyKumquatPtr == nullptr
Lookups by value are slower (linear time) than lookups by key. This is because the map is sorted by key, not by value. In addition, if a map has multiple keys with the same value, FindKey
may return any of them.
The GenerateKeyArray
and GenerateValueArray
functions populate a TArray
with a copy of all the keys and values respectively. In both cases, the array being passed is emptied before population, so the resulting number of elements will always equal the number of elements in the map.
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","" ]
Removal
Elements can be removed from maps by using the Remove
function and providing the key of the element to remove. The return value is the number of elements that were removed, and can be zero if the map didn't contain any elements matching the key.
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" }
// ]
Removing elements can leave holes in the data structure, which you can see when visualizing the map in Visual Studio's watch window, but they have been omitted here for clarity.
The FindAndRemoveChecked
function can be used to remove an element from the map and return its value. The "checked" part of the name indicates that the map will call check
(UE4's equivalent of assert
) if the key does not exist.
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!
The RemoveAndCopyValue
function is similar to Remove
, but copies the value of the removed element out to a reference parameter. If the key you specified is not present in the map, the output parameter will be unchanged and the function will return 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" }
// ]
Finally, you can remove all elements from the map with the Empty
or Reset
functions.
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 == []
Empty
and Reset
are similar, but Empty
can take a parameter to indicate how much slack to leave in the map, while Reset
always leaves as much slack as possible.Sorting
A TMap
can be sorted. After sorting, iteration over the map will present the elements in sorted order, but this behavior is only guaranteed until the next time you modify the map. Sorting is unstable, so equivalent elements in a MultiMap may appear in any order.
You can sort by key or by value using the KeySort
or ValueSort
functions, respectively. Both functions take a binary predicate which specifies the sort order.
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" }
// ]
Operators
Like TArray
, TMap
is a regular value type, and can be copied with the standard copy constructor or assignment operator. Maps strictly own their elements, so copying a map is deep; the new map will have its own copy of the elements.
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" }
// ]
TMap
supports move semantics, which can be invoked using the MoveTemp
function. After a move, the source map is guaranteed to be empty:
FruitMap = MoveTemp(NewMap);
// FruitMap == [
// { Key: 4, Value: "Kiwi" },
// { Key: 5, Value: "Apple" },
// { Key: 9, Value: "Melon" }
// ]
// NewMap == []
Slack
Slack is allocated memory that doesn't contain an element. You can allocate memory without adding elements by calling Reserve
, and you can remove elements without deallocating the memory they were using by calling Reset
or by calling Empty
with a non-zero slack parameter. Slack optimizes the process of adding new elements to the map by using pre-allocated memory instead of having to allocate new memory. It can also help with element removal since the system does not need to deallocate memory. This is especially efficient when you are emptying a map that you expect to repopulate immediately with the same number of elements or fewer.
TMap
does not provide a way of checking how many elements are preallocated the way the Max
function in TArray
does.
In this code, the Reserve
function preallocates our map to contain up to ten elements.
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" }
// ]
To remove all slack from a TMap
, use the Collapse
and Shrink
functions. Shrink
removes all slack from the end of the container, but this will leave any empty elements in the middle or at the start.
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
only removed one invalid element in the code above because there was only one empty element at the end. To remove all slack, the Compact
function should be called first, so that the empty spaces will be grouped together in preparation for 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
As long as a type has an operator==
and a non-member GetTypeHash
overload, you can use it as a key type for a TMap
without any changes. However, you may want to use types as keys without overloading those functions. In these cases, you can provide your own custom KeyFuncs
. To create KeyFuncs
for your key type, you must define two typedefs and three static functions, as follows:
KeyInitType
— Type used to pass keys around.ElementInitType
— Type used to pass elements around.KeyInitType GetSetKey(ElementInitType Element)
— Returns the key of an element.bool Matches(KeyInitType A, KeyInitType B)
— Returnstrue
ifA
andB
are equivalent,false
otherwise.uint32 GetKeyHash(KeyInitType Key)
— Returns the hash value ofKey
.
KeyInitType
and ElementInitType
are typedefs to the normal passing convention of the key type and element type. Usually these will be a value for trivial types and a const reference for non-trivial types. Remember that the element type of a map is a TPair
.
An example of a custom KeyFuncs
might look like this:
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
features a unique identifier, as well as some other data that does not contribute to its identity. GetTypeHash
and operator==
would be inappropriate here, because operator==
should not ignore any of the type's data for general-purpose usage, but would simultaneously need to do so in order to be consistent with the behavior of GetTypeHash
, which only looks at the UniqueID
field. The following steps will help to create a custom KeyFuncs
for FMyStruct
:
-
First, inherit
BaseKeyFuncs
, as it helpfully defines some types, includingKeyInitType
andElementInitType
.BaseKeyFuncs
takes two template parameters: the element type of the map, and the type of our key. As with all maps, the element type is aTPair
, takingFMyStruct
as itsKeyType
and the template parameter ofTMyStructMapKeyFuncs
as itsValueType
. The replacementKeyFuncs
is a template, so that you can specifyValueType
on a per-map basis, rather than needing to define a newKeyFuncs
every time you want to create aTMap
keyed onFMyStruct
. The secondBaseKeyFuncs
argument is the type of the key, not to be confused with theKeyType
fromTPair
, the Key field of the element stores. Since this map should useUniqueID
(fromFMyStruct
) as its key,FString
is used here. -
Next, define the three required
KeyFuncs
static functions. The first isGetSetKey
, which returns the key for a given element type. Our element type isTPair
, and our key isUniqueID
, so the function can simply returnUniqueID
directly.The second static function is
Matches
, which takes the keys of two elements (retrieved byGetSetKey
), and compares them to see if they are equivalent. ForFString
, the standard equivalence test (operator==
) is case-insensitive; to replace this with a case-sensitive search, use theCompare
function with the appropriate case-comparison option. -
Finally, the
GetKeyHash
static function takes an extracted key and returns a hashed value for it. Because theMatches
function is case-sensitive,GetKeyHash
must also be. A case-sensitiveFCrc
function will calculate the hash value from the key string. -
Now that the structure supports the behaviors that
TMap
requires, you can create instances of it.
KeyFuncs
parameter is last, and this TMap
type requires it. 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
// }
// ]
TMap
assumes that two items that compare as equals using Matches
will also return the same value from GetKeyHash
. In addition, modifying the key of an existing map element in a way which will change the results from either of these functions is considered undefined behavior, as this will invalidate map's internal hash. These rules also apply to the overloads of operator==
and GetKeyHash
when using the default KeyFuncs
.Miscellaneous
The CountBytes
and GetAllocatedSize
functions estimate how much memory the internal array is currently utilizing. CountBytes
takes an FArchive
parameter, while GetAllocatedSize
does not. These functions are typically used for stats reporting.
The Dump
function takes an FOutputDevice
and writes out some implementation information about the contents of the map. This function is usually used for debugging.