[언리얼 엔진] TSet

TSet

TSet는 순서가 중요하지 않은 상황에서 고유 엘리먼트를 저장하는데 사용되는 고속 컨테이너 클래스이다.

 

TSet는 TMap, TMultiMap과 비슷하지만 차이접이 있다. 독립된 키로 데이터를 연결하기 보다 데이터값 자체를 키로 사용하며 엘리먼트 값을 평가하는 오버라이드 가능 함수를 사용한다. TSet는 엘리먼트 추가,검색,제거가 매우 빠르다. TSet는 중복키를 지원하지 않지만, 템플릿 파라미터로 사용할 수 있다.

 

DefaultKeyFuncs에서 파생된 구조체는 해시 함수 기능을 제공하도록 지정 가능하다. 또한 한 Set에 값이 같은 키가 중복이 가능하도록 할 수도 있다. 데이터 저장을 위한 커스텀 메모리 얼로케이터를 제공할 수 있다.

 

TArray와 비슷하게 TSet는 동질성 컨테이너, 즉 모든 엘리먼트가 같은 유형이라는 뜻이다. TSet는 값 유형 이면서 복사, 할당, 소멸자 연산, TSet가 소멸되면 엘리먼트도 같이 소멸되도록 하는 오너십도 지원한다.

 

TSet는 해시를 사용한다. KeyFuncs 템플릿 파라미터가 제공된 경우, 키를 결정하는 방법, 키가 같은지 비교하는 방법, 키를 해싱하는 방법, 중복 키 허용 여부등을 지정할 수 있다. 기본적으로 키의 레퍼런스 반환, operator== 를 통한 값 비교, 해싱은 GetTypeHash 함수를 사용한다. 기본적으로 TSet는 중복 키를 허용하지 않는다. 키 유형이 위의 함수들을 지원하는 경우, 커스텀 KeyFuncs를 제공할 필요 없이 세트의 키로 사용할 수 있다. 커스텀 KeyFuncs는 DefaultKeyFuncs 구조체를 확장하여 작성한다.

 

TSet는 옵션 얼로케이터를 받아서 메모리 할당 방식을 제어할 수 있다. 세트에서 사용할 해시 버킷 수 및 엘리먼트 저장에 어떤 표준 UE4 얼로케이터를 사용할지 정의할 수 있다.

TSet의 Allocator 참조

 

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

세트 생성 및 채우기

TSet<FString> FruitSet;
FruitSet.Add(TEXT("Banana"));
FruitSet.Add(TEXT("Grapefruit"));
FruitSet.Add(TEXT("Pineapple"));
// FruitSet == [ "Banana", "Grapefruit", "Pineapple" ]

FString 데이터를 저장하는 빈 Tset가 생성된다. TSet는 operator==로 엘리먼트를 비교하고, GetTypeHash로 해싱하며, 표준 힙 얼로케이터를 사용한다. 생성시점에 할당되는 메모리는 없다. 세트를 채우는 일반적인 방식은 Add 함수에 키를 붙여 사용하는 것이다.

FruitSet.Add(TEXT("Pear"));
FruitSet.Add(TEXT("Banana"));
// FruitSet == [ "Banana", "Grapefruit", "Pineapple", "Pear" ]
// Note: Only one banana entry.

기본 얼로케이터를 사용하였기에 키는 고유성이 보장된다. 중복 키가 들어오면 기존에 있던 항목을 대체한다.

TArray와 마찬가지로 Add 대신에 Emplace를 사용하면 세트에 삽입할 때의 임시 생성을 피할 수 있다.

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

Append 함수를 사용하여 병합하는 것으로 다른 세트의 모든 엘리먼트를 삽입하는 것도 가능하다.

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

UPROPERTY TSet 편집

TSet에 UPROPERTY() 매크로와 편집 가능 키워드를 마킹하면 언리얼 에디터에서 엘리먼트를 추가 및 편집 할 수 있다.

UPROPERTY(Category = SetExample, EditAnywhere)
TSet<FString> FruitSet;

이터레이션

TSet에 대한 이터레이션(반복처리)은 TArray와 비슷하다. C++의 범위 for문을 사용하면 된다.

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

CreateIterator 또는 CreateConstIterators 함수로 이터레이터를 만들 수도 있다. CreateIterator는 읽기-쓰기가 가능하고, CreateConstIterators는 읽기 전용이다.

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

쿼리

Num 함수로 세트에 엘리먼트가 몇 개인지 확인 할 수 있다.
Contains 함수로 지정한 엘리먼트를 확인할 수 있다.

int32 Count = FruitSet.Num();
// Count == 8

bool bHasBanana = FruitSet.Contains(TEXT("Banana"));
bool bHasLemon = FruitSet.Contains(TEXT("Lemon"));
// bHasBanana == true
// bHasLemon == false

FSetElementId 구조체를 사용하여 세트 내 키 인덱스를 찾을 수 있다. operator[]로 그 인덱스를 사용하여 엘리먼트를 가져올 수 있다.

FSetElementId BananaIndex = FruitSet.Index(TEXT("Banana"));
// BananaIndex is a value between 0 and (FruitSet.Num() - 1)
FPlatformMisc::LocalPrint(
    *FString::Printf(
        TEXT(" \"%s\"\n"),
        *FruitSet[BananaIndex]
    )
);
// Prints "Banana"

FSetElementId LemonIndex = FruitSet.Index(TEXT("Lemon"));
// LemonIndex is INDEX_NONE (-1)
FPlatformMisc::LocalPrint(
    *FString::Printf(
        TEXT(" \"%s\"\n"),
        *FruitSet[LemonIndex]
    )
); // Assert!

Find 함수는 세트에 키가 있는 경우 엘리먼트 값으로의 포인터를, 없으면 널 포인터를 반환한다.

FString* PtrBanana = FruitSet.Find(TEXT("Banana"));
FString* PtrLemon = FruitSet.Find(TEXT("Lemon"));
// *PtrBanana == "Banana"
//  PtrLemon == nullptr

Array 함수는 TSet의 모든 엘리먼트 사본으로 채워진 TArray를 반환한다.

TArray<FString> FruitArray = FruitSet.Array();
// FruitArray == [ "Banana","Grapefruit","Pineapple","Pear","Orange","Kiwi","Melon","Mango" ] (order may vary)

제거

Remove 함수에 인덱스를 붙여 제거할 수 있지만 이터레이션 처리 도중에만 사용하기를 추천한다. Remove 함수는 제거된 엘리먼트 수를 반환한다. TSet가 중복 키를 지원하는 경우 일치하는 모든 엘리먼트를 제거한다.

FruitSet.Remove(0);
// FruitSet == [ "Grapefruit","Pineapple","Pear","Orange","Kiwi","Melon","Mango" ]

int32 RemovedAmountPineapple = FruitSet.Remove(TEXT("Pineapple"));
// RemovedAmountPineapple == 1
// FruitSet == [ "Grapefruit","Pear","Orange","Kiwi","Melon","Mango" ]
FString RemovedAmountLemon = FruitSet.Remove(TEXT("Lemon"));
// RemovedAmountLemon == 0

Empty나 Reset 함수로 모든 엘리먼트를 제거할 수 있다.
Empty는 남겨질 슬랙 양을 지정할 수 있고, Reset은 기존의 슬랙 양을 유지한다.

TSet<FString> FruitSetCopy = FruitSet;
// FruitSetCopy == [ "Grapefruit","Pear","Orange","Kiwi","Melon","Mango" ]

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

소팅

TSet는 소팅 가능하다. 소팅 이후에 세트를 반복처리 하면 소팅된 순서대로 나오나 세트가 변경되면 다시 소팅을 해야 순서가 보장된다. 소팅은 불안정해서 중복 키를 허용하는 TSet는 순서 없이 나타날 수 있다.
Sort 함수는 이항 술부를 받아서 소팅 순서를 지정한다.

FruitSet.Sort([](const FString& A, const FString& B) {
    return A > B; // sort by reverse-alphabetical order
});
// FruitSet == [ "Pear", "Orange", "Melon", "Mango", "Kiwi", "Grapefruit" ] (order is temporarily guaranteed)

FruitSet.Sort([](const FString& A, const FString& B) {
    return A.Len() < B.Len(); // sort strings by length, shortest to longest
});
// FruitSet == [ "Pear", "Kiwi", "Melon", "Mango", "Orange", "Grapefruit" ] (order is temporarily guaranteed)

연산자

TArray와 마찬가지로 TSet는 정규 값 유형으로 표준 복사 생성자 또는 할당 연산자를 통해 복사할 수 있다. 세트는 엘리먼트를 엄격하게 소유하기에 세트를 복사하면 심도가 유지되어 새 세트는 엘리먼트의 별도 사본을 갖는다.

TSet<int32, FString> NewSet = FruitSet;
NewSet.Add(TEXT("Apple"));
NewSet.Remove(TEXT("Pear"));
// FruitSet == [ "Pear", "Kiwi", "Melon", "Mango", "Orange", "Grapefruit" ]
// NewSet == [ "Kiwi", "Melon", "Mango", "Orange", "Grapefruit", "Apple" ]

슬랙

Slack 은 할당된 메모리에 엘리먼트가 없는 것을 말한다. 엘리먼트 없이 메모리를 할당하려면 Reserve를 호출하면 된다. 슬랙을 미리 할당하면 새 엘리먼트는 역순으로 추가된다.

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

FruitSet.Reserve(10);
for (int32 i = 0; i < 10; ++i)
{
    FruitSet.Add(FString::Printf(TEXT("Fruit%d"), i));
}
// FruitSet == [ "Fruit9", "Fruit8", "Fruit7" ... "Fruit2", "Fruit1", "Fruit0" ]

TSet에서 모든 슬랙을 제거하려면, Collapse 및 Shrink 함수를 사용하면된다.

// 세트에서 엘리먼트를 하나 건너 하나씩 제거합니다.
for (int32 i = 0; i < 10; i += 2)
{
    FruitSet.Remove(FSetElementId::FromInteger(i));
}
// FruitSet == ["Fruit8", <invalid>, "Fruit6", <invalid>, "Fruit4", <invalid>, "Fruit2", <invalid>, "Fruit0", <invalid> ]

FruitSet.Shrink();
// FruitSet == ["Fruit8", <invalid>, "Fruit6", <invalid>, "Fruit4", <invalid>, "Fruit2", <invalid>, "Fruit0" ]

FruitSet.CompactStable();
// FruitSet == ["Fruit8", "Fruit6", "Fruit4", "Fruit2", "Fruit0", <invalid>, <invalid>, <invalid>, <invalid> ]
FruitSet.Shrink();
// FruitSet == ["Fruit8", "Fruit6", "Fruit4", "Fruit2", "Fruit0" ]

DefaultKeyFuncs

한 유형에 operator==와 멤버가 아닌 GetTypeHash 오버로드가 있으면 그 유형은 TSet를 사용할 수 있다. 엘리먼트이면서 키이기 때문이다. 함수를 오버로드 하지 않으려면 커스텀 DefaultKeyFuncs를 제공하면 된다.

KeyInitType - 키 전달에 사용
ElementInitType - 엘리먼트 전달에 사
KeyInitType GetSetKey(ElementInitType Element) - 엘리먼트의 키를 반환하는데, 일반적으로 엘리먼트 자체이다.
bool Matches(KeyInitType A, KeyInitType B) - A와 B가 동일하면 true를 반환 아니면 false를 반환
uint32 GetKeyHash(KeyInitType Key) - 키의 해시 값을 반환 외부의 GetKeyHash 함수를 호출

문서

반응형

댓글

Designed by JB FACTORY