언리얼 C++ 프로그래밍

[언리얼 C++] 컨테이너 클래스 TMap 사용법(생성 및 삽입, 반복처리)

언린이 2021. 7. 10. 19:46

언리얼 엔진에서 TArray 컨테이너 다음으로 많이 사용되는 컨테이너는 TMap 컨테이너입니다.

TMap 컨테이너에 저장되는 엘리먼트는 Key 타입과 Value 타입, 두가지로 정의되며, TMap에 하나의 짝으로 저장됩니다.

 

TMap 컨테이너의 유형은 TMap 및 TMultiMap 두가지 입니다. 이 둘의 차이점은 TMap 컨테이너의 Key는 고유한 반면, TMultiMap 컨테이너는 다수의 동일한 Key 저장을 지원합니다. 즉, 기존에 존재하는 Key로 새로운 Key-Value 짝을 TMap 컨테이너에 추가하면 기존의 것이 대체되고, TMultiMap 컨테이너에 추가하면 새로 저장된다는 뜻입니다.

 

TMap 컨테이너에서 Key-Value 짝은 마치 개별 오브젝트인 것처럼 TMap의 엘리먼트 타입으로 정의됩니다. 이 글에서 엘리먼트란 Key-Value 짝을 뜻하는 반면, 개별 컴포넌트는 그 엘리먼트의 Key 또는 Value 중 하나를 말합니다. 엘리먼트 타입은 실제 TPair<KeyType, ElementType>입니다.

 

TArray 컨테이너처럼 TMap 컨테이너 역시 동질성 컨테이너로, 엘리먼트들은 전부 엄격히 같은 타입입니다. TMap 컨테이너는 일반적인 복사, 할당, 소멸 연산이 지원될 뿐만 아니라, 엘리먼트에 대한 강한 소유권이 지원되어 TMap 컨테이너가 소멸되면 엘리먼트도 같이 소멸됩니다.

 

TMap 컨테이너는 해시 컨테이너라서, 기본적으로 Key 타입은 반드시 GetTypeHash() 함수를 지원하고 Key의 동일성 비교를 위한 operator==를 지원합니다.

 

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

 

또 다른 TMap 템플릿 파라미터는 KeyFuncs으로, 엘리먼트 타입에서 Key를 구하는 방법, 두 Key 사이의 동등성을 비교하는 방법, Key에 대한 해싱 방법을 TMap에 알려주는 것입니다. 이에 대한 기본값은 그냥 Key에 대한 레퍼런스 반환, 동등성에는 operator== 지원, 해싱에는 GetTypeHash() 함수 호출입니다. Key 타입이 따로 특별한 함수를 지원하지 않는 경우, 커스텀 KeyFuncs을 제공할 필요는 없습니다.

 

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

 

 

1. TMap 컨테이너 생성 및 채우기

TMap<int32, FString> FruitMap;

TMap 컨테이너는 위 예제 코드처럼 생성할 수 있습니다.

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

 

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

TMap 컨테이너를 채우는 표준 방식은 Add() 함수에 Key와 Value를 포함해서 호출하는 것입니다.

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

 

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

현재 생성한 FruitMap은 TMultiMap 컨테이너가 아니라서 Key의 고유성이 보장됩니다. 위 예제 코드처럼 중복 Key 추가 시도를 하면, 이전에 Key가 2인 "Grapefruit" Value가 "Pear"로 대체되는 것을 볼 수 있습니다.

 

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

그리고 Add() 함수는 여러가지 형태로 오버로드되어 있어서 Value 없이 Key만을 받을 수도 있습니다.

Key만을 파라미터로 넘겨 Add() 함수를 호출하면 Value는 기본값으로 생성됩니다.

 

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

TMap 컨테이너에서도 TArray 컨테이너처럼 Add() 함수 대신 Emplace() 함수를 사용해서 TMap 삽입시의 임시 생성을 피할 수도 있습니다.

여기서 파라미터 두개를 Key 타입과 Value 타입 각각의 생성자에 직접 전달합니다. 여기서 int32에는 실제 효과가 없지만, Value에서는 임시 FString 생성을 피할 수 있습니다.

 

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

마지막으로, Append() 함수를 사용하여 다른 TMap 컨테이너에서 모든 엘리먼트를 삽입시켜 병합하는 것도 가능합니다.

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

 

 

2. TMap 컨테이너 반복처리

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")

TMap 컨테이너에 대한 반복처리는 TArray 컨테이너와 유사합니다. 엘리먼트 타입이 TPair 임을 기억하고, C++의 범위 for 기능을 사용하면 됩니다.

 

for (auto Iter = FruitMap.CreateConstIterator(); Iter; ++Iter)
{
    FPlatformMisc::LocalPrint(
        *FString::Printf(
            TEXT("(%d, \"%s\")\n"),
            Iter.Key(),   // same as Iter->Key
            *Iter.Value() // same as *Iter->Value
        )
    );
}

또한, CreateIterator() 함수 및 CreateConstIterator() 함수로 이터레이터를 만들 수도 있습니다. CreateIterator() 함수는 읽기-쓰기가 모두 가능한 이터레이터를 반환하는 반면, CreateConstIterator() 함수는 읽기 전용 이터레이터를 반환합니다. 어느 경우든, 해당 이터레이터의 Key() 함수와 Value() 함수를 사용해서 엘리먼트를 조사할 수 있습니다.