언리얼 - 자료구조 (TSet)

안영욱·2023년 2월 17일
0

언리얼

목록 보기
3/5

언리얼의 자료구조 TSet에대해 알아보겠습니다.
TArray``TMap와 과 비교하기도 하며 TMap에대해 어느정도 알필요가 있습니다.


TSetTMapTMultiMap과 다르게 엘리먼트 자체를 키로 사용한다.
TSet은 추가, 제거, 검색 기능이 굉장히 빠르다.
TSet은 기본적으로 중복키를 사용할수 없다. (템플릿 파라미터로 사용할수는 있다)


1. TSet

TSet순서가 중요하지않을때 사용할수있는 고속 컨테이너 자료형이다.
TSet은 여러 템플릿 파라미터를 이용하여 작동방식을 변경할수도 있다.
DefaultKeyFuncs 에서 파생된 구조체는 해시 함수 기능을 제공하도록 지정할 수있고, 중복된 키가 존재하도록 할수있다.
TSet 다른 컨테이너 클래스와 마찬가지로, 데이터 저장을 위한 커스텀 메모리 할당자를 제공할 수 있다.
TSet 은 해시를 사용하며 KeyFuncs 템플릿 파라미터가 제공된 경우 대표적으로 다음과 같은 항목들 을 지정할수 있다.

  • 키를 결정하는 방법
  • 두 키를 비교하는 방법
  • 키를 해싱하는 방법
  • 중복키 허용여부 등

TSet은 이러한 기능을 설정하지않아도 자동적으로 적절한 방식으로 처리된다.
TSet는 메모리 할당방식을 수정할수 있다 하지만 FHeapAllocator, TInlineAllocator언리얼 표준 할당자는 사용할수 없다.
대신 해시 버켓수, 엘리먼트 저장을 따르는 표준 할당자를 지정할수 있다.

TSetTArray와달리 순서를 보장받을수 없으며 메모리또한 인접하지 않을수 있다.
TSet의 데이터 구조는 희소배열로 엘리먼트 사이 간극을 효율적으로 사용할수 있다,
엘리먼트가 지워지면 이앞 간극을 앞으로 추가되는 엘리먼트가 채우기때문이다.


2. TSet를 생성하고 할당하기

TSet<FString> FruitSet; // FString을 
  • FString 타입을 저장하는 TSet을 생성, 초기화를 시켜주지않아 할당된 데이터는 없다.

2-2. Add

Add함수를 사용해 TSet에 데이터를 저장할수 있다.

FruitSet.Add(TEXT("Banana"));
FruitSet.Add(TEXT("Grapefruit"));
FruitSet.Add(TEXT("Pineapple"));
// FruitSet == [ "Banana", "Grapefruit", "Pineapple" ]
  • 삽입한 순서대로 배열에 저장되는것으로 보이지만 실제로는 순서를 보장받을수 없다.

기본 할당자를 사용하여 추가하였기에 키는 고유성을 보장받을수 있다, 이때 중복된 키를 저장하려고할때 발생하는 현상 ▽

FruitSet.Add(TEXT("Pear"));
FruitSet.Add(TEXT("Banana"));
// FruitSet == [ "Banana", "Grapefruit", "Pineapple", "Pear" ]
// Note: banana엘리먼트는 하나만 존재할수 있다.
  • Pear는 추가되었지만 Banana는 기존 엘리먼트를 갱신하여 할당되어 따로 추가되지않는다.

2-3. Emplace

TArray와 마찬가지로 Emplace함수를 사용하여 임시생성을 피할수있다.

FruitSet.Emplace(TEXT("Orange")); // 결과는 같으나 복사하여 대입하는 임시생성 과정을 거치지않는다.
// FruitSet == [ "Banana", "Grapefruit", "Pineapple", "Pear", "Orange" ]
  • 키유형 생성자의 인수가 직접 전달되어 임시생성을 피할수 있다.
  • TSet은 인수가 하나인 생성자로만 Emplace함수를 사용할수 있다.

2-4. Append

Append함수를 사용하여 TSet를 서로 병합할수 있다.

TSet<FString> FruitSet2;
FruitSet2.Emplace(TEXT("Kiwi"));
FruitSet2.Emplace(TEXT("Melon"));
FruitSet2.Emplace(TEXT("Mango"));
FruitSet2.Emplace(TEXT("Orange"));
FruitSet.Append(FruitSet2); // FruitSet에 FruitSet2를 병합시킴
// FruitSet == [ "Banana", "Grapefruit", "Pineapple", "Pear", "Orange", "Kiwi", "Melon", "Mango" ]
  • 중복되는 엘리먼트는 기존의 엘리먼트를 갱신시킨다.

2-5. 언리얼 에디터에서 TSet편집

TSetUPROPERTY() 매크로와 편집가능 키워드 (EditAnywhere, EditDefaultsOnly, EditInstanceOnly) 중 하나를 마킹하면, 언리얼 에디터에서 엘리먼트를 추가 및 편집 할 수 있습니다.

UPROPERTY(Category = SetExample, EditAnywhere) // UPROPERTY를 이용하여 에디터에서 수정이 가능하다.
TSet<FString> FruitSet;

3. 반복자

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"

3-1. CreateIterator, CreateConstIterators

CreateIteratorCreateConstIterators 함수로 반복자를 만들수 있다.

  • CreateIterator읽기 - 쓰기가 가능한 반환자
  • CreateConstIterators읽기만 가능한 반환자
for (auto It = FruitSet.CreateConstIterator(); It; ++It)
{
    FPlatformMisc::LocalPrint(
        *FString::Printf(
            TEXT("(%s)\n"),
            *It
        )
    );
}

4. 쿼리

4-1. Num

Num함수로 TSet에 엘리먼트가 얼마나 저장되어있는지 확인할수 있다.

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

4-2. Contains

Contains함수로 내가 지정한 값TSet존재하는지 확인할수 있다.

bool bHasBanana = FruitSet.Contains(TEXT("Banana"));
bool bHasLemon = FruitSet.Contains(TEXT("Lemon"));
// bHasBanana == true //banana가 존재하기때문에 true로 반환
// bHasLemon == false //Lemon이 존재하지않아 false로 반환

4-3. FSetElementId

FSetElementId 구조체로 내가 지정한 키 인덱스를 담을수있다, []와 함께사용하여 해당 인덱스에 접근해 엘리먼트를 가져올수있다.

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!

4-4. Find

Find함수를 사용하여 찾고자하는 키가 있다면 해당 엘리먼트의 pointer를, 없다면 nullpointer를 반환한다.

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

4-5. Array

array함수는 TSet의 모든 엘리먼트로 채워진 TArray로 반환한다.

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

5. 제거

5-1. Remove

Remove함수를 이용하여 엘리먼트를 제거할수있다, 하지만 이는 엘리먼트에 대한 반복처리도중에 사용되길 권장한다.
Remove함수는 제거한 엘리먼트 수를 반환한다.

FruitSet.Remove(0); //인덱스 번호로 제거를 할경우
// FruitSet == [ "Grapefruit","Pineapple","Pear","Orange","Kiwi","Melon","Mango" ] // 0번 인덱스의 엘리먼트 제거
int32 RemovedAmountPineapple = FruitSet.Remove(TEXT("Pineapple")); // 엘리먼트로 제거를 할경우
// RemovedAmountPineapple == 1 // 제거한 엘리먼트가 1개이기때문에 1을 반환
// FruitSet == [ "Grapefruit","Pear","Orange","Kiwi","Melon","Mango" ]
FString RemovedAmountLemon = FruitSet.Remove(TEXT("Lemon"));
// RemovedAmountLemon == 0 // Lemon은 존재하지않는 엘리먼트이기에 아무것도 지워지지않고 0을 반환

5-2. Empty, Reset

Empty 또는 Reset함수를 이용해 TSet모든엘리먼트를 비울수 있다.

TSet<FString> FruitSetCopy = FruitSet;
// FruitSetCopy == [ "Grapefruit","Pear","Orange","Kiwi","Melon","Mango" ]
FruitSetCopy.Empty();
// FruitSetCopy == []
  • Empty 의경우 TSet남겨둘 슬랙양을 정할수있다.
  • Reset 의경우 기존의 슬랙양을 유지한다.

6. 정렬(Sorting)

TSet은 정렬할수 있다, 하지만 정렬이후 배열이 수정된다면 더이상 그순서를 보장할순 없다.

기본적으로 TSet은 순서가 크게 상관없는경우 사용하기때문에 특별한 경우를 제외하곤 정렬할 이유가없다.

6-1. Sort

sort함수는 이항술부를 받아 정렬기준을 정할수 있다.

FruitSet.Sort([](const FString& A, const FString& B) {
    return A > B; // 알파벳 역순으로 정렬
});
// FruitSet == [ "Pear", "Orange", "Melon", "Mango", "Kiwi", "Grapefruit" ] (order is temporarily guaranteed)
FruitSet.Sort([](const FString& A, const FString& B) {
    return A.Len() < B.Len(); // 길이가 짧은 순서로 정렬
});
// FruitSet == [ "Pear", "Kiwi", "Melon", "Mango", "Orange", "Grapefruit" ] (order is temporarily guaranteed)

6-2. 연산자

TSetTArray처럼 값을 복사 생성연산자, 할당 연산자를 통해 복사할수 있다.

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

7. 슬랙

Slack은 메모리는 할당되었지만 엘리먼트는 없는 여유 공간을 의미한다.

7-1. Reserve

Reserve 함수를 사용하여 엘리먼트 없이 메모리는 할당할수 있다.

FruitSet.Reserve(10); // Reserve 함수로 먼저 길이가 10인 TSet을 선언해둠
for (int32 i = 0; i < 10; ++i)
{
    FruitSet.Add(FString::Printf(TEXT("Fruit%d"), i));
}
// FruitSet == [ "Fruit9", "Fruit8", "Fruit7" ... "Fruit2", "Fruit1", "Fruit0" ]

7-2. Collapse, Shrink

TSet에서 모든 슬랙을 제거하려면 Collpase 또는 Shrink를 사용하면된다.

  • Collpase함수는 배열내의 모든 Slack을 제거한다, 이때 불필요한 복사, 할당과정이있어 속도가 느릴수있다.
  • Shrink 함수는 배열의 끝부분의 모든 Slack을 제거한다,중간에 있는 슬랙은 제거하지 않는다.
// 세트에서 엘리먼트를 하나 건너 하나씩 제거합니다.
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" ] // 이때 제거된 슬랙은 배열 가장끝에있는 슬랙 하나뿐이다

7-3. Compact, CompactStable

Compact 혹은 CompactStable 함수는 배열내의 존재하는 Slack을 배열 끝으로 모두 모아준다, 이를 이용해 Compact함수이후 Shrink이용해 모든 슬랙을 제거할수 있다.

  • Compact함수는 모든 슬랙을 배열 끝으로 모아준다, 이때 배열내 데이터들의 순서는 고려하지않는다.
  • CompactStable 함수는 배열내 데이터들의 순서를 고려하고 모든 슬랙을 배열끝으로 모아준다.
FruitSet.CompactStable();
// 모든 슬랙을 뒤쪽으로 모아줌 이때 데이터의 순서를 고려함
// FruitSet == ["Fruit8", "Fruit6", "Fruit4", "Fruit2", "Fruit0", <invalid>, <invalid>, <invalid>, <invalid> ]
FruitSet.Shrink();
// 모든 슬랙이 제거됨
// FruitSet == ["Fruit8", "Fruit6", "Fruit4", "Fruit2", "Fruit0" ] 

출처

언리얼 공식 홈페이지


언리얼 자료구조 TSet에대해 작성해보았습니다
번역부분이 바로 이해가가지않는 부분은 제나름대로 수정해보았습니다.

profile
개발자좀 한번해보자

0개의 댓글