TSet
(세트)는 TMap
(맵) 및 TMultiMap
(멀티 맵)과 비슷하지만, 중요한 차이점이 있습니다: 독립된 키로 데이터 값을 연결하기 보다, TSet
는 데이터 값 자체를 키로 사용하며, 이 때 엘리먼트를 값을 평가하는 오버라이드 가능 함수를 사용합니다. TSet
는 엘리먼트 추가, 검색, 제거가 매우 빠릅니다 (고정비). 기본적으로 TSet
는 중복 키를 지원하지 않지만, 템플릿 파라미터로 사용할 수는 있습니다.
TSet
TSet
는 순서가 중요치 않은 상황에서 고유 엘리먼트를 저장하는 데 사용되는 고속 컨테이너 클래스입니다. 대부분의 경우 딱 하나의 파라미터 - 엘리먼트 유형- 만 필요합니다. 하지만 TSet
에 여러가지 템플릿 파라미터로 구성하여 작동방식을 변경하거나 다용도로 만들 수 있습니다. DefaultKeyFuncs
에서 파생된 구조체는 해시 함수 기능을 제공하도록 지정할 수 있을 뿐만 아니라, 한 세트에 값이 같은 키가 다수 존재할 수 있도록 할 수도 있습니다. 마지막으로, 다른 컨테이너 클래스와 마찬가지로, 데이터 저장을 위한 커스텀 메모리 얼로케이터를 제공할 수 있습니다.
TArray
와 비슷하게 TSet
는 동질성 컨테이너, 즉 그 엘리먼트 전부가 엄격히 같은 유형이라는 뜻입니다. TSet
는 값 유형이기도 하며, 일반적인 복사, 할당, 소멸자 연산뿐 아니라, TSet
가 소멸되면 엘리먼트도 같이 소멸되도록 하는 강 오너십도 지원합니다. 키 유형은 값 유형이기도 해야합니다.
TSet
는 해시를 사용하는데, KeyFuncs
템플릿 파라미터가 제공된 경우, 세트더러 엘리먼트에서 키를 결정하는 방법, 두 키가 같은지 비교하는 방법, 키를 해싱하는 방법, 중복 키 허용 여부 등을 지정할 수 있습니다. 이들은 기본값으로 키에 대한 레퍼런스 반환, 같은지 비교하는 데는 operator==
사용, 해싱에는 멤버가 아닌 GetTypeHash
함수를 사용합니다. 기본적으로 세트는 중복 키를 허용하지 않습니다. 키 유형이 이러한 함수를 지원하는 경우, 커스텀 KeyFuncs
를 제공할 필요 없이 세트 키로 사용할 수 있습니다. 커스텀 KeyFuncs
를 작성하려면, DefaultKeyFuncs
구조체를 확장하면 됩니다.
마지막으로, TSet
는 옵션 얼로케이터를 받아 메모리 할당 작동방식을 제어할 수 있습니다. 표준 UE4 얼로케이터는 (예로 FHeapAllocator
, TInlineAllocator
등) TSet
의 얼로케이터로 사용할 수 없습니다. 그 대신 세트 얼로케이터를 사용하는데, 세트에서 사용할 해시 버킷 수 및 엘리먼트 저장에 어떤 표준 UE4 얼로케이터를 사용할지 정의할 수 있습니다. 자세한 정보는 ``TSetAllocator
를 참고하세요.
TArray
와는 달리 TSet
엘리먼트의 메모리 내 상대 순서는 신뢰성이 있거나 안정적이지 않으며, 반복처리 결과도 처음 추가된 순서와 다르게 반환될 수 있습니다. 메모리에도 인접해서 놓이지 않을 수 있습니다. 세트의 데이터 구조는 희소 배열로, 엘리먼트 사이 간극을 효율적으로 지원하는 배열입니다. 세트에서 엘리먼트가 제거되면, 희소 배열에 간극이 나타납니다. 이 간극은 앞으로 추가되는 엘리먼트가 메꿉니다. TSet
가 엘리먼트를 섞어 간극을 채우지 않기는 해도 여전히 세트 엘리먼트에 대한 포인터가 무효화될 수 있는데, 전체 스토리지가 가득찬 상태에서 새 엘리먼트가 추가되면 재할당될 수가 있기 때문입니다.
세트 생성 및 채우기
TSet
생성 방식은 다음과 같습니다:
TSet<FString> FruitSet;
이렇게 하면 FString
데이터를 저장하는 빈 TSet
가 생성됩니다. TSet
는 operator==
로 엘리먼트를 직접 비교하고, GetTypeHash
로 해싱하며, 표준 힙 얼로케이터를 사용합니다. 이 시점에서 할당되는 메모리는 없습니다.
세트를 채우는 표준 방식은, Add
함수에 키 (엘리먼트) 를 붙여 사용하는 것입니다:
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: Only one banana entry.
이제 세트에 엘리먼트가 넷 들어있습니다. "Pear" 로 수가 3 에서 4 로 올랐지만, 새로운 "Banana" 는 세트의 엘리먼트 수에 변화를 주지 못했습니다. 전에 있던 "Banana" 항목을 대체했기 때문입니다.
TArray
와 마찬가지로 Add
대신 Emplace
를 사용하면 세트에 삽입할 때의 임시 생성을 피할 수 있습니다:
FruitSet.Emplace(TEXT("Orange"));
// FruitSet == [ "Banana", "Grapefruit", "Pineapple", "Pear", "Orange" ]
여기서 키 유형 생성자에 인수가 직접 전달됩니다. 그러면 그 값에 대한 임시 FString
이 생성되지 않습니다. TArray
와는 달리, 세트는 인수가 하나인 생성자로만 엘리먼트를 emplace 할 수 있습니다.
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" ]
여기서, 결과 세트는 Add
또는 Emplace
로 개별 추가하는 것과 같으며, 소스 세트의 중복 키는 타깃의 키를 대체합니다.
UPROPERTY TSet 편집
TSet
에 UPROPERTY()
매크로와 편집가능 키워드 (EditAnywhere
, EditDefaultsOnly
, EditInstanceOnly
) 중 하나를 마킹하면, 언리얼 에디터에서 엘리먼트를 추가 및 편집 할 수 있습니다.
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
는 읽기-쓰기 가능한 이터레이터를 반환하는 반면, CreateConstIterator
는 읽기 전용 이터레이터를 반환합니다. 어느 경우든, 이 이터레이터의 Key
및 Value
함수를 사용해서 엘리먼트를 조사할 수 있습니다. 우리 예제의 "fruit" 세트를 이터레이터로 출력하려면 다음과 같이 합니다:
for (auto It = FruitSet.CreateConstIterator(); It; ++It)
{
FPlatformMisc::LocalPrint(
*FString::Printf(
TEXT("(%s)\n"),
*It
)
);
}
쿼리
Num
함수로 세트에 엘리먼트가 몇 개나 저장되었는지 물어볼 수 있습니다:
int32 Count = FruitSet.Num();
// Count == 8
세트에 지정한 엘리먼트가 있는지 확인하려면, Contains
함수를 다음과 같이 호출합니다:
bool bHasBanana = FruitSet.Contains(TEXT("Banana"));
bool bHasLemon = FruitSet.Contains(TEXT("Lemon"));
// bHasBanana == true
// bHasLemon == false
FSetElementId
구조체를 사용하여 세트 내 키 인덱스를 찾을 수 있습니다. 그러면 operator[]
로 그 인덱스를 사용하여 엘리먼트를 가져올 수 있습니다. const 가 아닌 세트에 operator[] 를 호출하면 const 가 아닌 레퍼런스가 반환되고, const 세트에서 호출하면 const 레퍼런스가 반환됩니다.
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!
세트에 키가 있는지 확실하지 않은 경우, Contains
함수, 그리고 operator[]
를 사용해서 검사할 수 있습니다. 그러나 이게 이상적이지는 않은데, 성공적으로 가져오기 위해서는 같은 키에 조회를 두 번 해야 하기 때문입니다. Find
함수는 이 동작을 조회 단 한번으로 합칩니다. Find
는 맵에 키가 있는 경우 엘리먼트 값으로의 포인터를, 없으면 널 포인터를 반환합니다. const 세트에 Find
를 호출하면 반환하는 포인터도 const 가 됩니다.
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
함수는 제거된 엘리먼트 수를 반환하며, 제공된 키가 세트에 들어있지 않은 경우 0 이 됩니다. TSet
가 중복 키를 지원하는 경우, 일치하는 모든 엘리먼트를 제거합니다.
FruitSet.Remove(0);
// FruitSet == [ "Grapefruit","Pineapple","Pear","Orange","Kiwi","Melon","Mango" ]
엘리먼트 제거는 실제로 데이터 구조에 간극을 남길 수 있습니다. Visual Studio 의 감시 창에서 시트를 시각화시켜 보면 확인할 수 있지만, 여기서는 명료성을 위해 생략합니다.
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
함수로 모든 엘리먼트를 제거할 수 있습니다:
TSet<FString> FruitSetCopy = FruitSet;
// FruitSetCopy == [ "Grapefruit","Pear","Orange","Kiwi","Melon","Mango" ]
FruitSetCopy.Empty();
// FruitSetCopy == []
Empty
와 Reset
은 비슷하지만, Empty
는 맵에 남길 슬랙을 파라미터로 받을 수 있는 반면, Reset
은 항상 최대한의 슬랙을 남깁니다.소팅
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
는 정규 값 유형이므로 표준 복사 생성자 또는 할당 연산자를 통해 복사할 수 있습니다. 세트는 엘리먼트를 엄격하게 소유하므로, 세트를 복사하면 심도가 유지되어(deep), 새 세트는 엘리먼트 별도 사본을 갖습니다.
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
를 호출하면 되며, 메모리 할당을 해제하지(deallocate) 않고 엘리먼트를 제거하는 것도 Reset
호출 또는 Empty
에 0 이 아닌 슬랙 파라미터로 호출하면 됩니다. 메모리 할당 해제할 필요가 없으니 엘리먼트 제거에도 도움이 됩니다. 특히 세트를 비우고 엘리먼트 수가 같거나 적은 세트를 바로 다시 채우려는 경우 특히 효율적입니다.
TSet
는 TArray
의 Max
함수처럼 미리 할당된 엘리먼트 수를 검사하는 방법이 제공되지 않습니다.
다음 코드는 메모리 할당 해제 없이 세트에서 모든 엘리먼트를 제거하여 슬랙을 만듭니다:
FruitSet.Reset();
// FruitSet == [ <invalid>, <invalid>, <invalid>, <invalid>, <invalid>, <invalid> ]
슬랙을 직접 만들려면, 즉 엘리먼트를 추가하기 전 메모리를 미리 할당하려는 경우, Reserve
함수를 사용하세요.
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
함수를 사용하세요. 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" ]
Shrink
가 위 코드에서 유효하지 않은 엘리먼트를 단 하나만 제거했는데, 끝에 빈 엘리먼트가 하나뿐이었기 때문입니다. 모든 슬랙을 제거하려면 Compact
또는 CompactStable
함수를 먼저 호출하여 빈 공간을 그룹으로 묶고 Shrink
준비를 해야 합니다. 이름이 암시하듯, CompactStable
은 엘리먼트 순서를 유지한 채 통합합니다.
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
를 제공해 주면 됩니다. 키 유형에 대해 KeyFunc
를 만들려면, 다음과 같이 typedef 두 개와 static 함수 정의가 필요합니다:
KeyInitType
- 키 전달에 사용됩니다. 주로 ElementType 템플릿 파라미터에서 뽑힙니다.ElementInitType
- 엘리먼트 전달에 사용됩니다. 마찬가지, 주로 ElementType 템플릿 파라미터에서 뽑히므로, KeyInitType 과 같습니다.KeyInitType GetSetKey(ElementInitType Element)
- 엘리먼트의 키를 반환하는데, 일반적으로 엘리먼트 자체입니다.bool Matches(KeyInitType A, KeyInitType B)
- A 와 B 가 동일하면true
, 아니면false
를 반환합니다.uint32 GetKeyHash(KeyInitType Key)
- 키의 해시 값을 반환합니다. 보통 외부 GetTypeHash 함수를 호출합니다.
KeyInitType
및 ElementInitType
은 키/엘리먼트 유형의 일반 전달 규칙에 대한 typedef 입니다. 보통 이들은 사소한(trivial) 유형에 대해서는 값이, 사소하지 않은 유형에 대해서는 const 레퍼런스가 됩니다. 세트의 엘리먼트 유형은 키 유형이기도 하다는 점, 그래서 DefaultKeyFuncs 가 ElementType 라는 템플릿 파라미터 하나만 사용해서 둘을 정의하고 있다는 점 기억해 주세요.
TSet
는 (DefaultKeyFuncs
의) Matches
를 사용하여 동일성 비교를 하는 두 항목이 (KeyFuncs
의) GetKeyHash
에서도 같은 값을 반환한다 가정합니다.
TSet
의 내부 해시를 무효화시키기 때문입니다. 이 규칙은 DefaultKeyFuncs
기본 구현 사용시 GetKeyHash
및 operator==
의 오버로드에도 적용됩니다.기타
CountBytes
및 GetAllocatedSize
함수는 현재 내부 배열에 활용되고 있는 메모리 양을 측정합니다. CountBytes
는 FArchive
파라미터를 받는 반면, GetAllocatedSize
는 받지 않습니다. 이 함수는 일반적으로 통계 보고에 사용됩니다.
Dump
함수는 FOutputDevice 를 받아 세트의 내용 관련 약간의 구현 정보를 출력합니다. DumpHashElements
라는, 모든 해시 항목에서 모든 엘리먼트를 나열하는 함수도 있습니다. 이 함수들은 보통 디버깅에 사용됩니다.