언어:
페이지 정보
엔진 버전:
언리얼 엔진

TMap

언리얼 엔진

TArray 다음으로 언리얼 엔진에서 가장 자주 쓰이는 컨테이너는 TMap 입니다. 이 컨테이너는 연관성이 있는 컨테이너로, 모든 키에는 연관된 값이 있어서 값 오브젝트를 키를 통해 효율적으로 찾아볼 수 있다는 뜻입니다.

맵의 종류는 TMap 과 TMultiMap, 두 가지입니다. TMap 의 키는 고유하며, 이미 키가 있는 상태에서 새로운 키-값 짝을 삽입하면 기존 짝이 대체됩니다. TMultiMap 의 키는 고유하지 않아서, 새로 추가해도 기존 키가 대체되지 않습니다.

TMap

TMap 은 크게 키와 값, 두 가지 유형으로 정의되는데, 이 둘은 맵에 연관된 짝으로 저장됩니다. 이 짝에 대한 레퍼런싱은 마치 개별 오브젝트인 것처럼 맵의 엘리먼트 유형으로 편리하게 가능합니다. 이 문서에서 'element' (엘리먼트, 요소)란 키-값 짝을 뜻하는 반면, 개별 컴포넌트는 그 엘리먼트의 키 또는 값 중 하나를 말합니다. 엘리먼트 유형은 실제 TPair< KeyType, ElementType > 로, TPair 유형을 직접 가리키는 것은 드물긴 합니다.

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

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

TMap 은 메모리 할당 방식 제어를 위한 옵션 할당기를 받기도 합니다. 표준 언리얼 엔진 할당기 (예: FHeapAllocator, TInlineAllocator) 는 TMap 의 할당기로 사용할 수 없습니다. 대신 세트 할당기를 사용하는데, 맵에 해시 버킷을 얼마나 사용할지와 어떤 표준 UE 할당기를 사용해서 해시 및 엘리먼트를 저장할지를 정의합니다. 자세한 정보는 TSetAllocator 를 참고하세요.

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

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

맵 만들고 채우기

TMap 생성은 다음과 같이 가능합니다:

TMap<int32, FString> FruitMap;

정수에서 문자열로 매핑하도록 디자인된 공백 TMap 이 생성됩니다. 할당기도 KeyFuncs 도 지정하지 않았으므로, 맵은 표준 힙 할당이 되어 == 를 사용해서 키 (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 함수를 오버로드해서 값 없이 키를 받도록 했습니다. 키만 제공되면, 값은 기본값으로 생성됩니다:

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

TArray 처럼 Ad 대신 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"     }
// ]

여기서, 결과 맵은 Add/Emplace 를 사용하여 개별 추가시킨 것에 해당하므로, 소스 맵에서의 중복 키는 타깃의 것들을 대체합니다.

TMap 에 UPROPERTY() 매크로와 편집가능 키워드 (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")

맵은 보다 직접적인 반복처리 제어를 위한 별도의 이터레이터 유형을 제공하기도 합니다. CreateIterator 함수는 엘리먼트로의 읽고쓰기 접근을 제공하며, CreateConstIterator 함수는 읽기전용 접근을 제공합니다. 이터레이터 오브젝트 스스로 Key() 와 Value() 함수를 제공하여 키와 값을 접근할 수 있습니다. 코드에서 사용되는 형태는 다음과 같습니다:

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

[] 인덱싱 연산자에 키를 사용하여 주어진 키에 연관된 값에 대한 레퍼런스를 구할 수 있습니다. const 가 아닌 맵에 [] 연산자를 호출하면 const 가 아닌 레퍼런스가 반환되며, const 맵에 호출하면 const 레퍼런스가 반환됩니다. 키가 존재하지 않으면, 어서트가 발동됩니다:

FString Val7 = FruitMap[7];
// Val7 == "Pineapple"
FString Val8 = FruitMap[8]; // assert!

Contains 함수로 맵에 특정 키가 존재하는지 검사할 수 있습니다:

bool bHas7 = FruitMap.Contains(7);
bool bHas8 = FruitMap.Contains(8);
// bHas7 == true
// bHas8 == false

대부분 키의 존재 여부를 알지 못하는 상태에서 엘리먼트를 찾아보게 됩니다. Constains 에 [] 연산자를 붙여 사용하면, 키를 두 번 찾아본다는 뜻인데, 의도한 바는 아닙니다. Find 함수로 한 번의 룩업만 가능하며, 찾은 엘리먼트에 대한 레퍼런스 보다는 값으로의 포인터를 반환하고, 키가 존재하지 않으면 null 을 반환합니다:

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

const 맵에 호출되면, 반환되는 포인터 역시 const 입니다.

FindOrAdd 함수는 주어진 키를 검색하여 연관된 값으로의 레퍼런스를 반환하며, 키가 존재하지 않는 경우 생성자 기본값으로 추가한 뒤 그에 대한 레퍼런스를 반환합니다. 추가될 수가 있어서, 이 함수는 const 맵에는 호출할 수 없습니다:

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 함수는 이름과는 달리 키를 검색하여 레퍼런스가 아닌 값을 반환합니다. 키를 찾았으면, 연관된 값에 대한 사본이 반환되며, 찾지 못한 경우 생성자 기본 값 유형이 반환됩니다. FindOrAdd 와 비슷한 동작을 보이나, FindRef 함수는 레퍼런스가 아닌 값을 반환하므로, 맵이 변경되지 않아 const 오브젝트에서 호출 가능합니다:

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 함수는 (값을 주면 키를 찾는) 반대 룩업을 합니다. 이 함수 사용시 주의할 점은, 키와는 달리 값은 해시가 되지 않아 키에 대한 룩업은 선형 연산이기 때문입니다. 또한, 값이 고유하다는 보장이 없어, 특정 값에 대해 반환되는 키는 맵에 중복 값이 있는 경우 제멋대로일 것입니다.

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

GenerateKeyArray / GenerateValueArray 함수는 배열을 각각 모든 키 / 값 사본으로 채웁니다. 두 경우에, 전달되는 배열은 채우기 전 비워지므로, 엘리먼트 최종 수는 맵의 엘리먼트 수와 항상 같습니다:

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 함수에 제거할 엘리먼트 키를 넣어주는 것으로 맵에서 엘리먼트를 제거할 수 있습니다:

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' 이름 부분은 존재 여부를 검사한 다음, 존재하지 않으면 어서트가 발생한다는 뜻입니다:

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 함수는 비슷한 작업을 하지만, 제거할 값 유형에 대한 레퍼런스를 받은 다음 키를 찾았는지 아닌지 부울 값을 반환합니다. 실행시간 오류가 나지 않고 빠진 키에 사용하는 것이 가능합니다. 키를 찾지 못했으면, 호출은 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 함수를 통해 모든 엘리먼트를 제거할 수 있습니다:

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

FruitMapCopy.Empty();
// FruitMapCopy == []

TArray 처럼 Empty 는 주어진 엘리먼트 갯수로 맵을 다시 채우려는 경우 최적화에 사용할 수 있는 슬랙 값을 옵션으로 받습니다.

소팅

TMap 은 임시 소팅이 가능합니다. 맵에 대한 다음 번 반복처리는 엘리먼트를 소팅된 순서대로 제공해 주지만, 앞으로 맵에 변경을 가하게 되면 맵의 소팅이 흐트러지게 마련입니다. 소팅은 불안정하므로, 상응하는 엘리먼트가 어떤 순서로든 나타날 수 있습니다.

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

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

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

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

슬랙

TMap 역시 슬랙이란 개념이 있어서, 맵 채우기 최적화에 사용 가능합니다. Reset 함수는 Empty() 호출처럼 작동하나, 기존 엘리먼트에 사용된 메모리를 해제하지는 않습니다:

FruitMap.Reset();
// FruitMap == [<invalid>, <invalid>, <invalid>]

여기서 Empty 와 같은 방식으로 맵을 비웠지만, 보관에 사용된 메모리가 해제되지 않고 슬랙으로 남습니다.

TMap 은 TArray::Max() 처럼 몇 개의 엘리먼트가 미리 할당되었는지 검사하는 방법을 제공하지 않긴 하지만, 미리 할당 슬랙을 지원하기는 합니다. 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" }
// ]

참고로 슬랙은 새 엘리먼트가 역순으로 추가되도록 합니다. 맵의 엘리먼트 순서는 어떤 식으로든 믿을 수 없다는 예가 되기도 합니다.

Shrink 함수는 컨테이너 끝부분에 낭비된 슬랙을 제거한다는 점에서 TArray 버전과 마찬가지로 작동합니다. 하지만 TMap 의 데이터 구조에는 '구멍'이 있을 수 있으므로, 이 함수는 구조체 끝에 남은 구멍의 슬랙 제거만 가능합니다:

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 호출에서 유효하지 않아 제거된 엘리먼트가 딱 하나입니다. 끝부분에 구멍이 딱 하나였기 때문입니다. Shrink 전 Compact 함수를 사용하면 모든 구멍을 제거하는 것이 가능합니다:

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 는 2 개의 형 정의와 3 개의 스태틱 함수 정의를 요합니다:

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

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

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

  • bool Matches(KeyInitType A, KeyInitType B) - A 와 B 의 동일 여부를 반환합니다.

  • uint32 GetKeyHash(KeyInitType Key) - 키의 해시 값을 반환합니다.

KeyInitType 과 ElementInitType 은 키 유형과 엘리먼트 유형의 일반적인 전달 규칙에 대한 형 정의입니다. 보통 사소한 유형에는 값이, 사소하지 않은 유형에는 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);
    }
};

상태의 일부분으로 고유 식별자를 가지면서, 식별에는 기여하지 않는 다른 상태도 몇 가지 있는 유형입니다. GetTypeHash 와 == 연산자는 여기에 적합하지 않습니다. == 연산자가 상태의 일부분을 무시하는 것은 거짓말이 되고, GetTypeHash 도 == 연산자에 일치해야 하는데, == 연산자가 제대로 정의되었다면 불가능한 일이기 때문입니다. 하지만 맵에서 이러한 유형 식별 목적으로는 상태의 UniqueID 부분만 사용해도 좋습니다.

  1. 먼저 BaseKeyFuncs 를 상속합니다. KeyInitType 와 ElementInitType 을 포함해서 유용한 것을 몇 가지 정의해 주기 때문입니다. 나머지 구현에서도 사용할 수 있도록 Super 에서 파생 클래스로 직접 끌어옵니다.

    BaseKeyFuncs 는 두 개의 템플릿 파라미터를 받는데, 맵의 엘리먼트 유형 및 키 유형과, GetSetKey 에서 반환되는 오브젝트 입니다. 모든 맵과 마찬가지로 엘리먼트 유형은 TPair 이며, KeyType 으로 FMyStruct 를 받고, ValueType 으로 TMyStructMapKeyFuncs 의 템플릿 파라미터를 받습니다. 대체 KeyFuncs 를 템플릿으로 만들어 맵별로 ValueType 을 지정할 수 있도록 하여, FMyStruct 에 키 설정된 TMap 을 만들고자 할 때마다 KeyFuncs 를 새로 정의할 필요가 없도록 했습니다. 두 번째 BaseKeyFuncs 인수는 키 유형으로, 엘리먼트의 Key 필드에 저장되는 TPair 의 'KeyType' 과는 혼동하지 말아야 합니다. FMyStruct::UniqueID 를 키로 사용하려 하기에, 여기에 FString 을 지정합니다.

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

    두 번째 스태틱 함수는 Matches 로, 이미 GetSetKey 를 사용하여 엘리먼트 유형에서 추출된 두 엘리먼트의 키를 받아, 그 둘을 비교하여 같은지 봅니다. FString 의 == 연산자는 대소문자를 구분하지 않는데, 우리는 대소문자 구분 검색을 원하니 FString::Compare 함수에 옵션을 적절히 붙여 줍니다.

  3. 마지막으로 GetKeyHash 스태틱 함수는 추출된 키를 받아 그에 대한 해시된 값을 반환합니다. 여기서도 FString 에 대한 GetTypeHash 동작은 대소문자를 무시하므로, 대소문자를 구분하는 FCrc 함수를 호출하여 해시 계산을 합니다.

  4. 이제 새로운 KeyFuncs 를 사용하여 TMap 을 만들 수 있습니다. 할당기도 제공해 줘야 하는데, KeyFuncs 파라미터가 마지막이지만, 그냥 기본값을 대체하도록 하겠습니다:

    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 은 KeyFuncs::Matches 를 사용해서 동등성 검사를 하는 두 항목이 KeyFuncs::GetKeyHash 에서와 같은 값을 반환한다 가정합니다. 게다가 이 함수들 중 어느 하나의 결과를 바꾸는 방식으로 기존 맵 엘리먼트의 키를 변경하는 것은 정의되지 않은 동작으로 간주되는데, 그렇게 되면 TMap 의 내부 해시가 무효화되기 때문입니다. 이 규칙은 기본 KeyFuncs 사용시 == 연산자와 GetKeyHash 의 오버로드시에도 적용됩니다.

기타

CountBytes 와 GetAllocatedSize 함수는 현재 내부 배열에 사용되는 메모리 양을 측정하는 데 사용됩니다. CountBytes 는 FArchive 를 받아 GetAllocatedSize 를 직접 호출 가능합니다. 주로 통계 보고에 사용됩니다.

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

관련 토픽