언리얼 C++ 프로그래밍

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

언린이 2021. 7. 13. 21:21

언리얼 엔진에서 TSet 컨테이너는 TMap 컨테이너 및 TMultiMap 컨테이너와 비슷하지만, 중요한 차이점이 있습니다.

독립된 Key로 Value를 연결하기 보다, TSet 컨테이너는 Value 자체를 Key로 사용합니다.

그리고 TSet 컨테이너는 엘리먼트의 추가, 검색 및 제거가 굉장히 빠릅니다.

 

그래서 TSet 컨테이너는 순서가 중요하지 않은 상황에서 고유 엘리먼트를 저장하고 싶을때, 고속으로 엘리먼트의 추가, 검색, 제거가 요구될때 많이 사용됩니다.

TSet 컨테이너를 생성할때 엘리먼트의 타입만이 필요합니다. 하지만 TSet 컨테이너에 여러가지 템플릿 파라미터로 구성하여 작동방식을 변경하거나 다용도로 만들 수 있습니다. DefaultKeyFuncs에서 파생된 구조체는 해시 함수 기능을 제공하도록 지정할 수 있을 뿐만 아니라, 한 TSet 컨테이너에 값이 같은 Key가 다수 존재할 수 있도록 할 수도 있습니다. 마지막으로, 다른 컨테이너 클래스와 마찬가지로, 데이터 저장을 위한 커스텀 메모리 얼로케이터를 제공할 수 있습니다.

 

TSet 컨테이너는 TArray 컨테이너와 비슷하게 동질성 컨테이너입니다. 즉, 포함된 엘리먼트 전부가 엄격히 같은 타입이라는 뜻입니다. TSet 컨테이너는 일반적인 복사, 할당, 소멸자 연산뿐 아니라, TSet 컨테이너가 소멸되면 엘리먼트도 같이 소멸되도록 하는 강한 소유권도 지원합니다.

 

그리고 TSet 컨테이너는 해시를 사용하는데 KeyFuncs 템플릿 파라미터가 제공된 경우, TSet 컨테이너에 엘리먼트에서 Key를 결정하는 방법, 두 Key가 같은지 비교하는 방법, Key를 해싱하는 방법, 중복 Key 허용 여부 등을 지정할 수 있습니다. 이들은 기본값으로 Key에 대한 레퍼런스 반환, 같은지 비교하는 데는 operator== 사용, 해싱에는 멤버가 아닌 GetTypeHash() 함수를 사용합니다. 기본적으로 TSet 컨테이너는 중복 Key를 허용하지 않습니다. Key로 지정할 타입이 이러한 함수들을 지원하는 경우, 커스텀 KeyFuncs을 제공할 필요는 없습니다. 커스텀 KeyFuncs을 제작하려면, DefaultKeyFuncs 구조체를 확장하면 됩니다.

 

또한, TSet 컨테이너는 옵션 얼로케이터를 받아 메모리 할당 작동 방식을 제어할 수 있습니다. 표준 UE4 얼로케이터 FHeapAllocator이나 TInlineAllocator는 TSet 컨테이너의 얼로케이터로 사용할 수 없습니다. 그 대신 TSet 얼로케이터를 사용하는데, TSet 컨테이너에서 사용할 해시 버킷 수 및 엘리먼트 저장에 어떤 표준 UE4 얼로케이터를 사용할지 정의할 수 있습니다.

 

마지막으로, TSet 컨테이너는 TArray 컨테이너와 달리, TSet 엘리먼트의 메모리 내 상대 순서는 신뢰성이 없고 안정적이지 않습니다. 그래서 반복처리 결과도 처음 추가된 순서와 다르게 반환될 수 있습니다. 메모리에도 인접하게 놓이지 않을 수 있습니다. TSet 컨테이너의 데이터 구조는 희소 배열로, 엘리먼트 사이 간극을 효율적으로 지원하는 배열입니다. TSet 컨테이너에서 엘리먼트가 제거되면, 희소 배열에 간극이 나타납니다. 이 간극은 앞으로 추가되는 엘리먼트가 메꾸게 됩니다. TSet 컨테이너가 엘리먼트를 섞어 간극을 채우지 않기는 해도 여전히 TSet 엘리먼트에 대한 포인터가 무효화될 수 있는데, 전체 스토리지가 가득찬 상태에서 새 엘리먼트가 추가되면 재할당될 수가 있기 때문입니다.

 

 

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

TSet<FString> FruitSet;

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

이렇게 하면 FString 데이터를 저장하는 빈 TSet 컨테이너가 생성됩니다. TSet 컨테이너는 operator==로 엘리먼트를 직접 비교하고, GetTypeHash() 함수로 해싱하며, 표준 힙 얼로케이터를 사용합니다. 이 시점에서 할당되는 메모리는 없습니다.

 

FruitSet.Add(TEXT("Banana"));
FruitSet.Add(TEXT("Grapefruit"));
FruitSet.Add(TEXT("Pineapple"));
// FruitSet = { "Banana", "Grapefruit", "Pineapple" }

TSet 컨테이너를 채우는 표준 방식은, Add() 함수에 파라미터로 Key를 넘겨 호출하는 것입니다.

위 예제 코드에서 추가된 엘리먼트들은 삽입 순서대로 나열되어 있기는 하지만, 실제로 이 순서가 유지된다는 보장은 없습니다. 새로 만든 TSet 컨테이너의 경우, 삽입 순서가 유지될 수도 있지만, 삽입과 제거가 많아질 수록 TSet 컨테이너에 새로 추가한 엘리먼트가 컨테이너의 끝에 오지 않을 확률이 높아집니다.

 

FruitSet.Add(TEXT("Pear"));
FruitSet.Add(TEXT("Banana"));
// FruitSet = { "Banana", "Grapefruit", "Pineapple", "Pear" }

TSet 컨테이너를 생성할때 기본 얼로케이터를 사용했으므로, TSet 컨테이너의 Key는 고유성이 보장됩니다.

이미 TSet 컨테이너에 존재하는 Key를 추가하면 위 예제 코드처럼 중복으로 추가되지 않고 변화가 일어나지 않습니다.

 

FruitSet.Emplace(TEXT("Orange"));
// FruitSet = { "Banana", "Grapefruit", "Pineapple", "Pear", "Orange" }

TSet 컨테이너에서도 TArray 컨테이너와 마찬가지로 Add() 함수 대신 Emplace() 함수를 사용하면 TSet 컨테이너에 엘리먼트를 삽입할때의 임시 생성을 피할 수 있습니다.

여기서 Key 타입 생성자에 파라미터가 직접 전달됩니다. 그러면 그 값에 대한 임시 FString이 생성되지 않습니다.

여기서 TArray 컨테이너와 다른 점은 TSet 컨테이너는 파라미터가 하나인 생성자를 가진 타입의 엘리먼트만 Emplace() 함수로 추가할 수 있다는 것입니다.

 

TSet<FString> FruitSet2;
FruitSet2.Emplace(TEXT("Kiwi"));
FruitSet2.Emplace(TEXT("Melon"));
FruitSet2.Emplace(TEXT("Mango"));
FruitSet2.Emplace(TEXT("Orange"));
FruitSet.Append(FruitSet2);
// FruitSet = { "Banana", "Grapefruit", "Pineapple", "Pear", "Orange", "Kiwi", "Melon", "Mango" }

마지막으로, Append() 함수를 사용하여 다른 TSet 컨테이너의 모든 엘리먼트들을 병합하는 것도 가능합니다.

위 예제 코드에서 결과 TSet 컨테이너는 Add() 함수 또는 Emplace() 함수를 사용해 FruitSet2의 각 엘리먼트를 개별 추가한 것과 같습니다. 그리고 FruitSet2의 엘리먼트 중 기존 FruitSet에 중복되는 것이 있으면, 기존의 엘리먼트가 대체됩니다.

 

 

2. TSet 컨테이너 반복처리

for (auto& Elem : FruitSet)
{
    FPlatformMisc::LocalPrint(
        *FString::Printf(
            TEXT(" \"%s\"\n"),
            *Elem
        )
    );
}
// Output:
//  "Banana"
//  "Grapefruit"
//  "Pineapple"
//  "Pear"
//  "Orange"
//  "Kiwi"
//  "Melon"
//  "Mango"

TSet 컨테이너에 대한 반복처리는 TArray 컨테이너와 유사합니다. C++의 범위 for 문을 사용하면 됩니다.

 

for (auto Iter = FruitSet.CreateConstIterator(); Iter; ++Iter)
{
    FPlatformMisc::LocalPrint(
        *FString::Printf(
            TEXT("(%s)\n"),
            *Iter
        )
    );
}

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