언리얼 C++ 프로그래밍

[언리얼 C++] 컨테이너 클래스 TMap 사용법(연산자, 슬랙, KeyFuncs 지정)

언린이 2021. 7. 12. 19:18

TMap 컨테이너 클래스의 기본적인 설명과 간단한 사용법은 [언리얼 C++] 컨테이너 클래스 TMap 사용법(생성 및 삽입, 반복처리) (tistory.com) 글을 참고하시기 바랍니다.

이 글에서는 TMap 컨테이너 클래스의 연산자, 슬랙, KeyFuncs에 대해 알아보겠습니다.

 

 

1. 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" }
// }

TArray 컨테이너처럼 TMap 컨테이너는 정규 값 타입이므로, 표준 복사 생성자나 할당 연산자를 통해 복사가 가능합니다. TMap 컨테이너는 자신의 엘리먼트를 엄격히 소유하므로, TMap 컨테이너를 복사하면 새로운 TMap 컨테이너는 별도의 엘리먼트 사본을 가지게 됩니다.

 

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

TMap 컨테이너에는 MoveTemp()라는 함수가 존재합니다. 해당 함수를 사용하여 파라미터로 넘겨준 TMap 컨테이너의 엘리먼트들을 다른 TMap 컨테이너로 이동시킬 수 있습니다. 이때, 엘리먼트들을 받을 TMap 컨테이너에 엘리먼트가 존재하더라도 모두 지우고 파라미터로 넘겨준 TMap 컨테이너의 엘리먼트들만 가지게 됩니다.

이동 이후 파라미터로 넘겨준 TMap 컨테이너는 빈 상태가 됩니다.

 

 

2. TMap 컨테이너 슬랙 지원

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

 

FruitMap.Reserve(10);
for (int32 index = 0; index < 10; ++index)
{
    FruitMap.Add(index, FString::Printf(TEXT("Fruit%d"), index));
}
// FruitMap = {
//  { Key: 9, Value: "Fruit9" },
//  { Key: 8, Value: "Fruit8" },
//  ...
//  { Key: 1, Value: "Fruit1" },
//  { Key: 0, Value: "Fruit0" }
// }

위 예제 코드에서 Reserve() 함수를 사용하여 TMap 컨테이너에 엘리먼트 10개를 저장할 수 있도록 미리 메모리를 할당합니다.

 

for (int32 index = 0; index < 10; index += 2)
{
    FruitMap.Remove(index);
}
// 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" }
// }

TMap 컨테이너에서 슬랙을 제거하려면 Collapse() 함수 및 Shrink() 함수를 사용하면 됩니다.

Shrink() 함수는 컨테이너 끝의 모든 슬랙을 제거하지만, 중간이나 시작 부분의 빈 엘리먼트는 남겨둡니다.

위 예제 코드에서 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" }
// }

그래서 모든 슬랙을 제거하려면, Compact() 함수를 먼저 호출해서 빈 공간을 컨테이너의 끝에 그룹으로 묶어주고 Shrink() 함수를 호출하여 컨테이너의 끝에 존재하는 빈 엘리먼트들을 제거하면 됩니다.

 

 

3. TMap 컨테이너 KeyFuncs 지정

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

  • KeyInitType : Key 전달에 사용됩니다.
  • ElementInitType : 엘리먼트 전달에 사용됩니다.
  • KeyInitType GetSetKey(ElementInitType Element) : 엘리먼트의 Key를 반환합니다.
  • bool Matches(KeyInitType A, KeyInitType B) : A 와 B 가 동일하면 true, 아니면 false 를 반환합니다.
  • uint32 GetKeyHash(KeyInitType Key) : Key의 해시 값을 반환합니다. 보통 외부 GetTypeHash() 함수를 호출합니다.

KeyInitType 및 ElementInitType은 Key 타입과 엘리먼트 타입의 일반 전달 규칙에 대한 typedef입니다. 보통 이들은 trivial 타입에 대해서는 값이, 그렇지 않은 타입에 대해서는 const 레퍼런스가 됩니다.

 

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);
    }
};

위 예제 코드는 커스텀 KeyFuncs의 예제입니다.

FMyStruct에는 고유 식별자는 물론, 정체성에 기여하지 않는 몇가지 다른 데이터도 들어있습니다. GetTypeHash() 함수 및 operator==은 여기 적합하지 않을텐데, 왜냐하면 operator==은 일반적인 용도로 사용되는 타입의 데이터를 무시해서는 안되는 반면 그와 동시에 GetTypeHash() 함수의 동작, 즉 UniqueID 필드만 살펴 보는 동작 일관성을 위해서는 필요합니다. 다음 단계는 FMyStruct에 대한 커스텀 KetFuncs 생성에 도움이 됩니다.

  1. 먼저 BaseKeyFuncs을 상속합니다. KeyInitType와 ElementInitType을 포함해서 유용한 것을 몇 가지 정의해 주기 때문입니다. BaseKeyFuncs 는 두가지 템플릿 파라미터, TMap 컨테이너의 엘리먼트 타입과 Key 타입을 받습니다. 엘리먼트 타입은 TPair 이며, FMyStruct를 KeyType으로, TMyStructMapKeyFuncs을 ValueType으로 받습니다. 대체 KeyFuncs은 템플릿이므로, FMyStruct에 Key 설정된 TMap 컨테이너를 생성하려 할 때마다 KeyFuncs을 새로 정의하기 보다는, TMap 컨테이너 단위로 ValueType을 지정할 수 있습니다. 두번째 BaseKeyFuncs 인수는 Key 타입이며, 엘리먼트가 저장하는 Key 필드인 TPair의 KeyType과는 다릅니다. 이 TMap 컨테이너는 (FMyStruct 의) UniqueID를 Key로 사용해야 하므로, 여기에는 FString이 사용됩니다.
  2. 다음, 필수 KeyFuncs 스태틱 함수 셋을 정의합니다. 첫째는 GetSetKey() 함수로, 엘리먼트 타입을 주면 Key를 반환합니다. 엘리먼트 타입은 TPair, Key는 UniqueID 이므로, 함수는 UniqueID 를 직접 반환할 수 있습니다. 두번째 스태틱 함수는 Matches() 함수로, (GetSetKey 로 불러온) 두 엘리먼트의 Key를 받아, 그 둘을 비교하여 동등성 검사를 합니다. FString 의 경우, 표준 동등성 검사 (operator==) 는 대소문자를 구분하지 않으니, 구분하기 위해 Compare 함수에 적합한 대소문자 비교 옵션을 붙여 줍니다.
  3. 마지막으로, GetKeyHash() 스태틱 함수는 추출한 Key를 받아 그 해시 값을 반환합니다. Matches() 함수는 대소문자를 구분하므로, GetKeyHash() 함수도 구분해야 합니다. 대소문자를 구분하는 FCrc 함수는 Key 스트링에서 해시 값을 계산합니다.
  4. 구조체가 TMap 컨테이너에 필요한 동작을 지원하므로, 그 인스턴스를 만들면 됩니다.

KeyFuncs을 제공할때 주의할 점이라면, TMap 컨테이너는 Matches() 함수를 사용해서 동등성 검사를 하는 두 항목이 GetKeyHash() 함수에서와 같은 값을 반환한다 가정합니다. 게다가 이 함수들 중 어느 하나의 결과를 바꾸는 방식으로 기존 TMap 컨테이너 엘리먼트의 Key를 변경하는 것은 정의되지 않은 동작으로 간주되는데, 그렇게 되면 TMap 컨테이너의 내부 해시가 무효화되기 때문입니다.