TMap

TMap, 맵은 크게 키 유형과 값 유형, 두 가지로 정의되며, 맵에 하나의 짝으로 저장됩니다.

Windows
MacOS
Linux

언리얼 엔진 4 (UE4)에서 TArray (배열) 다음으로 가장 자주 사용되는 컨테이너는 TMap (맵)입니다. TMapTSet (세트)와 비슷한 점은 그 구조하 키 해시 기반이라는 점입니다. 그러나 TSet 와 달리 이 컨테이너는 데이터를 키-값 짝으로 (TPair<KeyType, ValueType>) 저장하며, 저장 및 불러올 때는 키만 사용합니다.

맵의 유형은 두 가지, TMapTMultiMap 입니다. 이 둘의 차이점은 TMap 키는 고유한 반면, TMultiMap 은 다수의 동일한 키 저장을 지원합니다. 기존 짝과 일치하는 키로 새 키-값 짝을 TMap 에 추가하면 기존 것이 대체되고, TMultiMap 에 추가하면 새로 저장합니다.

TMap

TMap 에서 키-값 짝은 마치 개별 오브젝트인 것처럼 맵의 엘리먼트 유형으로 정의됩니다. 이 문서에서 element (요소, 엘리먼트)란 키-값 짝을 뜻하는 반면, 개별 컴포넌트는 그 엘리먼트의 키 또는 값 중 하나를 말합니다. 엘리먼트 유형은 실제 TPair< KeyType, ElementType > 로, TPair 유형을 직접 가리키는 것은 드물긴 합니다.

TArray 처럼 TMap 역시 동질성 컨테이너로, 그 엘리먼트 전부 엄격히 같은 유형입니다. TMap 은 값 우형이기도 하여, 일반적인 복사, 할당, 소멸 연산이 지원될 뿐만 아니라, 그 엘리먼트에 대한 강 소유권도 지원되어 맵이 소멸되면 같이 소멸되기도 합니다. 키 유형과 값 유형은 반드시 값 유형이어야 합니다.

TMap 은 해시 컨테이너라서, 기본적으로 키 유형은 반드시 GetTypeHash 를 지원하고 키의 동일성 비교를 위한 operator== 가 제공되어야 한다는 뜻입니다. 해시에 대해서는 나중에 자세히 다루겠습니다.

TMap 은 메모리 할당 방식 제어를 위한 옵션 얼로케이터를 받기도 합니다. 그러나 TArray 와 달리 이들은 세트 얼로케이터를 사용하며, 표준 UE4 얼로케이터를 (예: FHeapAllocator, TInlineAllocator) 사용할 수 없습니다. 세트 얼로케이터 (TSetAllocator 클래스)는 맵에 해시 버킷을 얼마나 사용할지와 어떤 표준 UE 얼로케이터를 사용해서 해시 및 엘리먼트를 저장할지를 정의합니다.

마지막 TMap 템플릿 파라미터는 KeyFuncs 로, 엘리먼트 유형에서 키를 구하는 방법, 두 키 사이의 동등성을 비교하는 방법, 키에 대한 해싱 방법을 맵에 알려주는 것입니다. 이에 대한 기본값은 그냥 키에 대한 레퍼런스 반환, 동등성에는 operator== 사용, 해싱에는 멤버가 아닌 GetTypeHash 함수 호출 등입니다. 키 유형이 이러한 함수를 지원하는 경우, 커스텀 KeyFuncs 를 제공할 필요 없이 맵 키로 사용 가능합니다.

TArray 와는 달리 메모리 내 TMap 엘리먼트의 상대 순서는 안정적이거나 신뢰할 수가 없어서, 엘리먼트에 대한 반복처리시 추가된 것과 다른 순서로 반환되기 일쑤입니다. 엘리먼트가 메모리에 연속해서 놓이지도 잘 않습니다. 맵의 배후 데이터 구조는 성긴 배열, 즉 엘리먼트 사이 간극을 효율적으로 지원하는 배열입니다. 맵에서 엘리먼트가 제거됨에 따라, 성긴 배열에 간극이 생기며, 이후 엘리먼트를 추가하면 채워지게 됩니다. 여기서 TMap 이 간극을 채울 때 엘리먼트를 섞지는 않지만 맵 엘리먼트로의 포인터가 여전히 유효하지는 않을 수 있는데, 맵이 가득차서 새 일리먼트가 추가된 경우 전체 스토리지가 재할당될 수 있기 때문입니다.

맵 만들고 채우기

TMap 생성 방법은 다음과 같습니다:

TMap<int32, FString> FruitMap;

FruitMap 은 이제 정수 키로 식별되는 빈 스트링 TMap 이 됩니다. 얼로케이터도 KeyFuncs 도 지정하지 않았으므로, 맵은 표준 힙 할당이 되어 operator== 를 사용해서 (int32 유형) 키 비교를 하고, GetTypeHash 를 사용해서 해싱합니다. 이 시점에서 할당되는 메모리는 없습니다.

맵을 채우는 표준 방식은 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"  }
// ]

여기 나열된 엘리먼트는 삽입 순이지만, 이 엘리먼트의 순서가 실제로 보장되지는 않습니다. 새 맵의 경우 삽입 순서대로 있을 수 있지만, 맵에 삽입이나 제거가 많을 수록 새 엘리먼트가 끝에 오지 않을 확률이 높습니다.

TMultiMap 이 아니라서 키의 고유성이 보장됩니다. 중복 키를 추가 시도하면 어떤 일이 벌어지는지 볼 수 있습니다:

FruitMap.Add(2, TEXT("Pear"));
// FruitMap == [
//  { Key: 5, Value: "Banana"    },
//  { Key: 2, Value: "Pear"      },
//  { Key: 7, Value: "Pineapple" }
// ]

맵에는 여전이 엘리먼트가 세 개 있지만, 이전에 키가 2 인 "Grapefruit" 값이 "Pear" 로 대체되었습니다.

Add 함수를 오버로드해서 값 없이 키를 받도록 했습니다. 이렇게 오버로드한 Add 가 호출되면, 값은 기본값으로 생성됩니다:

FruitMap.Add(4);
// FruitMap == [
//  { Key: 5, Value: "Banana"    },
//  { Key: 2, Value: "Pear"      },
//  { Key: 7, Value: "Pineapple" },
//  { Key: 4, Value: ""          }
// ]

TArray 처럼 Add 대신 Emplace 를 사용해서 맵 삽입시의 임시 생성을 피할 수도 있습니다:

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"    }
// ]

여기서 인수 두 개를 키 유형과 값 유형 각각의 생성자에 직접 전합니다. 여기서 int32 에는 실제 효과가 없지만, 값에는 임시 FString 생성을 피할 수 있습니다. TArray 와는 달리 단일 인수 생성자로 맵속에 엘리먼트를 Emplace 시키는 것만 가능합니다.

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 은 비었습니다.

위 예제에서 결과 맵은 Add 또는 Emplace 를 사용해 FruitMap2 각 엘리먼트를 개별 추가하고, 프로세스가 완료되면 FruitMap2 를 비운 것과 같습니다. 즉 FruitMap2 의 엘리먼트 중 기존 FruitMap 의 엘리먼트 키를 공유하는 것이 있으면 그 엘리먼트를 대체한다는 뜻입니다.

TMapUPROPERTY 매크로와 편집가능 키워드 (EditAnywhere, EditDefaultsOnly, EditInstanceOnly) 중 하나를 마킹하면, 언리얼 에디터에서 엘리먼트를 추가 및 편집 가능합니다.

UPROPERTY(Category = MapsAndSets, EditAnywhere)
TMap<int32, FString> FruitMap;

반복처리

TMap 에 대한 iteration(반복처리)는 TArray 와 유사합니다. 엘리먼트 유형이 TPair 임을 기억하고, C++ 의 범위 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")

CreateIteratorCreateConstIterators 함수로 이터레이터를 만들 수도 있습니다. CreateIterator 는 읽기-쓰기 가능한 이터레이터를 반환하는 반면, CreateConstIterator 는 읽기-전용 이터레이터를 반환합니다. 어느 경우든, 이 이터레이터의 KeyValue 함수를 사용해서 엘리먼트를 조사할 수 있습니다. 다음은 이터레이터를 사용해서 예제 "fruit" 맵의 내용을 출력한 것입니다:

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);
bool bHas8 = FruitMap.Contains(8);
// bHas7 == true
// bHas8 == false

맵에 특정 키가 있다는 것을 안다면, 키를 인덱스로 하여 operator[] 로 해당 값을 조회하면 됩니다. non-const 맵은 non-const 레퍼런스를, const 맵은 const 레퍼런스를 반환합니다.

맵에 키가 들어있는지 확인한 후 operator[] 를 사용해야 합니다. 맵에 키가 들어있지 않다면 어서트가 발생합니다.

FString Val7 = FruitMap[7];
// Val7 == "Pineapple"
FString Val8 = FruitMap[8];
// 어서트!

맵에 키가 들어있는지 확실하지 않다면, Contains 함수에 operator[] 를 사용하면 됩니다. 그러나 이는 이상적이지 않은데, 불러오기에 성공해도 같은 키를 두 번 조회해야 하기 때문입니다. Find 함수는 이 동작을 한 번의 조회로 합칩니다. Find 는 맵에 키가 들어있으면 엘리먼트 값으로의 포인터를, 없으면 널 포인터를 반환합니다. Find 를 const 맵에 호출하면 반환하는 포인터도 const 가 됩니다.

FString* Ptr7 = FruitMap.Find(7);
FString* Ptr8 = FruitMap.Find(8);
// *Ptr7 == "Pineapple"
//  Ptr8 == nullptr

또는 쿼리 결과 유효성을 보장하려면, FindOrAdd 또는 FindRef 를 사용하면 됩니다. FindOrAdd 는 제공한 키에 연관된 값으로의 레퍼런스를 반환합니다. 키가 맵에 있지 않은 경우, FindOrAdd 는 새로 생성된 엘리먼트에 키와 기본 생성된 값을 반환하고, 맵에 추가합니다. 이게 맵을 수정할 수도 있기 때문에, FindOrAdd 는 non-const 맵에만 사용할 수 있습니다. FindRef 는 그 이름과 달리 키에 연관된 값, 맵에 그 키가 없는 경우 기본 생성된 값 사본을 반환합니다. FindRef 는 새 엘리먼트를 생성하지 않으므로, const 및 non-const 맵 양쪽에서 사용 가능합니다. FindOrAddFindRef 는 맵에서 키를 찾지 못해도 성공하므로, 미리 Contains 검사나 반환 값의 null 검사와 같은 통상의 안전 확인 절차 없이 호출해도 안전합니다.

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: ""          }
// ]

우리 예제의 Ref8 초기화에서 한 것처럼 FindOrAdd 는 맵에 새 항목을 추가할 수 있으므로, 이전에 (Find 에서) 얻은 포인터 또는 (FindOrAdd 에서 얻은) 레퍼런스는 유효성이 상실될 수 있습니다. 새 엘리먼트의 저장을 위해 맵의 백엔드 스토리지를 확장시켜야 하는 경우 추가로 발생하는 메모리 할당 및 기존 데이터 이동 작업의 결과입니다. 위 예제에서 Ref7FindOrAdd(8) 호출 이후 Ref8 이후에 유효성을 상실할 수 있습니다.

FindKey 함수는 역조회, 즉 제공된 값에 일치하는 키를 검색하여 제공된 값의 짝인 첫 키로의 포인터를 반환합니다. 맵에 있지 않은 값을 검색하면 널 키를 반환합니다.

const int32* KeyMangoPtr   = FruitMap.FindKey(TEXT("Mango"));
const int32* KeyKumquatPtr = FruitMap.FindKey(TEXT("Kumquat"));
// *KeyMangoPtr   == 5
//  KeyKumquatPtr == nullptr

값 조회는 키 조회보다 (선형 시간으로) 느립니다. 맵의 소팅 기준은 키이지 값이 아니기 때문입니다. 게다가 맵에 값이 같은 키가 여럿인 경우, FindKey 가 뭘 반환할지는 알 수 없습니다.

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 함수에 제거할 엘리먼트 키를 넣어주는 것으로 맵에서 엘리먼트를 제거할 수 있습니다. 반환 값은 제거된 엘리먼트 개수이며, 맵에 키와 일치하는 엘리먼트가 없는 경우 0 이 될 수 있습니다.

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"     }
// ]

엘리먼트를 제거하면 데이터 구조에 간극이 생겨, Visual Studio 의 감시창에서 맵을 시각화시켜 보면 확인할 수 있지만, 여기서는 명확성을 위해 생략했습니다.

FindAndRemoveChecked 함수는 맵에서 엘리먼트를 제거하고 그 값을 반환하는 데 사용됩니다. 이름의 "checked" 부분은 키가 존재하지 않으면 맵이 (UE4 에서 assert 에 해당하는) check 를 호출함을 나타냅니다.

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);
// 어서트!

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"  }
// ]

마지막으로, Empty 또는 Reset 함수로 맵에서 모든 엘리먼트를 제거할 수 있습니다.

TMap<int32, FString> FruitMapCopy = FruitMap;
// FruitMapCopy == [
//  { Key: 5, Value: "Mango"  },
//  { Key: 4, Value: "Kiwi"   },
//  { Key: 3, Value: "Orange" },
//  { Key: 9, Value: "Melon"  }
// ]

FruitMapCopy.Empty();       // 여기서 Reset() 을 호출해도 됩니다.
// FruitMapCopy == []

EmptyReset 은 비슷하지만, Empty 는 맵에 남겨 둘 슬랙 양을 지정할 수 있는 반면, Reset 은 항상 슬랙을 최대한 남깁니다.

소팅

TMap 은 소팅 가능합니다. 소팅 이후 맵을 반복처리하면 엘리먼트가 소팅된 순서대로 나오지만, 이 순서는 맵을 수정하면 보장되지 않습니다. 소팅은 불안정하므로, MultiMap 의 엘리먼트가 어떤 순서로 나올지는 알 수 없습니다.

KeySort 또는 ValueSort 함수를 사용하여 각각 키 / 값 소팅이 가능하며, 두 함수 모두 소팅 순서를 나타내는 이항 술부를 받습니다:

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 은 정규 값 유형이므로, 표준 복사 생성자나 할당 연산자를 통해 복사 가능합니다. 맵은 자신의 엘리먼트를 엄격히 소유하므로, 맵을 복사하면 심도가 유지되어(deep) 새 맵은 별도의 엘리먼트 사본을 갖게 됩니다.

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" }
// ]

TMapMoveTemp 함수 사용시 호출 가능한 이동 시맨틱 역시도 지원합니다. 이동 이후 소스 맵은 빈 상태가 보장됩니다:

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

슬랙

Slack (여유분, 슬랙)은 할당된 메모리에 엘리먼트가 없는 것을 말합니다. 엘리먼트 없이 메모리를 할당하려면 Reserve 를 호출하면 되며, 메모리 할당을 해제하지(deallocate) 않고 엘리먼트를 제거하는 것도 Reset 호출 또는 Empty 에 0 이 아닌 슬랙 파라미터로 호출하면 됩니다. 메모리 할당 해제할 필요가 없으니 엘리먼트 제거에도 도움이 됩니다. 특히 맵을 비우고 엘리먼트 수가 같거나 적은 맵으로 바로 다시 채우려는 경우 특히 효율적입니다.

TMapTArrayMax 함수처럼 미리 할당된 엘리먼트 수를 검사하는 방법이 제공되지 않습니다.

이 코드에서, Reserve 함수는 맵에 엘리먼트 10 개를 저장할 수 있도록 미리 할당합니다.

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" }
// ]

TMap 에서 슬랙을 제거하려면 CollapseShrink 함수를 사용합니다. Shrink 는 컨테이너 끝의 모든 슬랙을 제거하지만, 중간이나 시작 부분의 빈 엘리먼트는 남겨 둡니다.

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

한 유형에 operator== 와 멤버가 아닌 GetTypeHash 오버로드가 있는 한, 그 유형은 변경 없이 TMap 의 키 유형으로 사용해도 됩니다. 하지만 그 함수 오버로드 없이 유형을 키로 사용하고 싶은 경우가 있습니다. 이러한 경우, 별도의 커스텀 KeyFuncs 를 제공해 주면 됩니다. 키 유형에 대해 KeyFunc 를 만들려면, 다음과 같이 두 개의 typedef 및 세 개의 static 함수 정의가 필요합니다:

  • KeyInitType - 키 전달에 사용됩니다.

  • ElementInitType - 엘리먼트 전달에 사용됩니다.

  • KeyInitType GetSetKey(ElementInitType Element) - 엘리먼트의 키를 반환합니다.

  • bool Matches(KeyInitType A, KeyInitType B) - A 와 B 가 동일하면 true, 아니면 false 를 반환합니다.

  • uint32 GetKeyHash(KeyInitType Key) - 키의 해시 값을 반환합니다. 보통 외부 GetTypeHash 함수를 호출합니다.

KeyInitTypeElementInitType 은 키 유형과 엘리먼트 유형의 일반 전달 규칙에 대한 typedef 입니다. 보통 이들은 사소한(trivial) 유형에 대해서는 값이, 사소하지 않은 유형에 대해서는 const 레퍼런스가 됩니다. 맵의 엘리먼트 유형은 TPair 라는 점 기억하세요.

커스텀 KeyFuncs 예제는 이렇습니다:

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 에는 고유 식별자는 물론, 정체성에 기여하지 않는 몇 가지 다른 데이터도 들어 있습니다. GetTypeHashoperator== 는 여기 적합하지 않을 텐데, 왜냐면 operator== 는 일반적인 용도로 사용되는 유형의 데이터를 무시해서는 안되는 반면 그와 동시에 GetTypeHash 동작, 즉 UniqueID 필드만 살펴 보는 동작 일관성을 위해서는 필요합니다. 다음 단계는 FMyStruct 에 대한 커스텀 KeyFuncs 생성에 도움이 됩니다:

  1. 먼저 BaseKeyFuncs 를 상속합니다. KeyInitTypeElementInitType 을 포함해서 유용한 것을 몇 가지 정의해 주기 때문입니다.

    BaseKeyFuncs 는 두 가지 템플릿 파라미터, 맵의 엘리먼트 유형과 키 유형을 받습니다. 모든 맵과 마찬가지로 엘리먼트 유형은 TPair 이며, FMyStructKeyType 으로, TMyStructMapKeyFuncsValueType 으로 받습니다. 대체 KeyFuncs 는 템플릿이므로, FMyStruct 에 키 설정된 TMap 을 생성하려 할 때마다 KeyFuncs 를 새로 정의하기 보다는, 맵 단위로 ValueType 을 지정할 수 있습니다. 두 번째 BaseKeyFuncs 인수는 키 유형이며, 엘리먼트가 저장하는 Key 필드인 TPairKeyType 과는 다릅니다. 이 맵은 (FMyStruct 의) UniqueID 를 키로 사용해야 하므로, 여기에는 FString 이 사용됩니다.

  2. 다음, 필수 KeyFuncs 스태틱 함수 셋을 정의합니다. 첫째는 GetSetKey 로, 엘리먼트 유형을 주면 키를 반환합니다. 엘리먼트 유형은 TPair, 키는 UniqueID 이므로, 함수는 UniqueID 를 직접 반환할 수 있습니다.

    두 번째 스태틱 함수는 Matches 로, (GetSetKey 로 불러온) 두 엘리먼트의 키를 받아, 그 둘을 비교하여 동등성 검사를 합니다. FString 의 경우, 표준 동등성 검사 (operator==) 는 대소문자를 구분하지 않으니, 구분하기 위해 Compare 함수에 적합한 대소문자 비교 옵션을 붙여 줍니다.

  3. 마지막으로, GetKeyHash 스태틱 함수는 추출한 키를 받아 그 해시 값을 반환합니다. Matches 함수는 대소문자를 구분하므로, GetKeyHash 도 구분해야 합니다. 대소문자를 구분하는 FCrc 함수는 키 스트링에서 해시 값을 계산합니다.

  4. 구조체가 TMap 에 필요한 동작을 지원하므로, 그 인스턴스를 만들면 됩니다.

이 예제에서는 기본 세트 얼로케이터를 지정했습니다. 왜냐면 KeyFuncs 파라미터가 마지막이고, 이 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 를 제공할 때 주의할 점이라면, TMapMatches 를 사용해서 동등성 검사를 하는 두 항목이 GetKeyHash 에서와 같은 값을 반환한다 가정합니다. 게다가 이 함수들 중 어느 하나의 결과를 바꾸는 방식으로 기존 맵 엘리먼트의 키를 변경하는 것은 정의되지 않은 동작으로 간주되는데, 그렇게 되면 TMap 의 내부 해시가 무효화되기 때문입니다. 이 규칙은 기본 KeyFuncs 를 사용할 때 operator==GetKeyHash 의 오버로드에도 적용됩니다.

기타

CountBytesGetAllocatedSize 함수는 현재 내부 배열에 사용되는 메모리 양을 측정합니다. CountBytesFArchive 파라미터를 받는 반면 GetAllocatedSize 는 아닙니다. 이 함수들은 일반적으로 통계 보고에 사용됩니다.

Dump 함수는 FOutputDevice 를 받아 맵 콘텐츠에 대한 약간의 구현 정보를 출력합니다. 이 함수는 주로 디버깅에 사용됩니다.

새로운 언리얼 엔진 4 문서 사이트에 오신 것을 환영합니다!

문서 사이트에 대한 의견을 모을 수 있는 피드백 시스템을 포함해서 여러가지 새로운 기능을 준비하고 있습니다. 아래 Documentation Feedback 포럼(영문) 또는 언리얼 엔진 네이버 공식 카페(한글) 중 편하신 곳에 의견이나 문제점을 알려 주세요.

새 시스템이 준비되면 알려 드리겠습니다.

네이버 카페
공식 포럼