TSet는 TMap 및 TMultiMap과 비슷하지만, 중요한 차이점이 있는데, 그건 바로 TSet는 독립된 키로 데이터 값을 연결하는 대신 데이터 값 자체를 키로 사용하며, 이때 요소를 평가하는 오버라이드 가능 함수를 사용한다는 점입니다. TSet는 요소를 매우 빠르게(상수 시간) 추가하고 검색하고 제거합니다. 기본적으로 TSet는 중복 키를 지원하지 않지만, 템플릿 파라미터를 사용하여 이 행동을 활성화할 수 있습니다.
TSet
TSet는 순서가 중요치 않은 상황에서 고유 요소를 저장하는 데 사용되는 고속 컨테이너 클래스입니다. 대부분의 사용 사례에서는 요소 타입이라는 하나의 파라미터만 필요합니다. 그러나, TSet를 다양한 템플릿 파라미터로 구성하여 행동을 변경하고 더 다양하게 활용할 수 있습니다. DefaultKeyFuncs를 기반으로 파생된 구조체를 지정하여 해싱 함수 기능을 제공하고 값이 같은 여러 개의 키가 세트에 존재할 수 있도록 할 수 있습니다. 마지막으로, 다른 컨테이너 클래스와 마찬가지로 데이터 스토리지를 위한 커스텀 메모리 얼로케이터를 제공할 수 있습니다.
TArray와 마찬가지로 TSet는 동질성 컨테이너입니다. 즉, 모든 요소가 엄격하게 동일한 타입입니다. TSet는 값 타입이기도 하며, 일반적인 복사, 할당, 소멸자 연산은 물론 TSet가 소멸하면 요소도 같이 소멸하도록 하는 강력한 오너십도 지원합니다. 키 타입은 값 타입이기도 해야 합니다.
TSet는 해시를 사용합니다. KeyFuncs 템플릿 파라미터가 제공된 경우, 이를 사용하여 요소에서 키를 결정하는 방법과 두 키가 같은지 비교하는 방법, 키를 해싱하는 방법, 중복 키를 허용할지 여부를 세트에 알려줄 수 있습니다. 디폴트는 키에 대한 레퍼런스를 반환하고 operator==를 사용하여 동일성을 확인하고 멤버가 아닌 GetTypeHash 함수를 호출하여 해싱하는 것입니다. 기본적으로 이 세트는 중복 키를 허용하지 않습니다. 키 타입이 이러한 함수를 지원한다면, 커스텀 KeyFuncs를 제공할 필요 없이 세트 키로 사용할 수 있습니다. 커스텀 KeyFuncs를 작성하려면, DefaultKeyFuncs 구조체를 확장하면 됩니다.
마지막으로 TSet는 옵션으로 얼로케이터를 받아 메모리 할당 행동을 제어할 수 있습니다. FHeapAllocator나 TInlineAllocator 같은 표준 언리얼 엔진 4(UE4) 얼로케이터는 TSet의 얼로케이터로 사용할 수 없습니다. 대신 TSet는 세트 얼로케이터를 사용하여 세트가 사용할 해시 버킷 수와 요소 스토리지에 사용할 표준 UE4 얼로케이터를 정의합니다. 자세한 내용은 TSetAllocator를 참조하세요.
TArray와 달리 메모리에서 TSet 요소의 상대적 순서는 신뢰할 수 없거나 안정적이지 않으며, 요소를 반복작업하면 추가된 순서와 다른 순서로 반환될 가능성이 높습니다. 또한, 요소는 메모리에 연속적으로 배치되지 않을 가능성이 높습니다. 세트의 백업 데이터 구조체는 요소 사이의 간격을 효율적으로 지원하는 배열인 희소 배열입니다. 세트에서 요소가 제거되면 희소 배열의 간격이 나타납니다. but this will leave any empty elements in the middle or at the start.그런 다음, 배열에 새 요소를 추가하면 이러한 간격을 메울 수 있습니다. 그러나, 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 편집
UPROPERTY 매크로와 '편집 가능' 키워드(EditAnywhere, EditDefaultsOnly 또는 EditInstanceOnly) 중 하나를 TSet에 마킹하면, 언리얼 에디터에서 요소를 추가하고 편집할 수 있습니다.
UPROPERTY(Category = SetExample, EditAnywhere)
TSet<FString> FruitSet;반복작업
TSet에 대한 반복작업은 TArray와 비슷합니다. C++ 범위 기반 for 문을 사용하면 됩니다.
for (auto& Elem : FruitSet)
{
FPlatformMisc::LocalPrint(
*FString::Printf(
TEXT(" \"%s\"\n"),
*Elem
)
);
}
// Output:
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 == falseFSetElementId 구조체를 사용하여 세트 내 키의 인덱스를 찾을 수 있습니다. 그런 다음, 해당 인덱스를 operator[]와 함께 사용하여 요소를 얻을 수 있습니다. operator[]를 const가 아닌 세트에서 호출하면 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"
세트에 키가 포함되어 있는지 확실하지 않은 경우 Contains 함수를 사용하여 확인한 다음, operator[]를 사용할 수 있습니다. 그러나, 검색에 성공하려면 동일한 키를 두 번 조회해야 하므로 이 방법은 최적과는 거리가 멉니다. Find 함수는 이러한 행동을 한 번의 조회로 결합합니다. Find는 세트에 키가 포함되어 있으면 요소의 값에 대한 포인터를 반환하고, 포함되어 있지 않으면 null 포인터를 반환합니다. const 세트에서 Find를 호출하면 반환하는 포인터도 const가 됩니다.
FString* PtrBanana = FruitSet.Find(TEXT("Banana"));
FString* PtrLemon = FruitSet.Find(TEXT("Lemon"));
// *PtrBanana == "Banana"
// PtrLemon == nullptrArray 함수는 TSet의 모든 요소 사본으로 채워진 TArray를 반환합니다. 전달한 배열은 연산을 시작할 때 비워지므로, 결과 요소 수는 항상 세트의 요소 수와 같습니다.
TArray<FString> FruitArray = FruitSet.Array();
// FruitArray == [ "Banana","Grapefruit","Pineapple","Pear","Orange","Kiwi","Melon","Mango" ] (order may vary)제거
Remove 함수에 인덱스를 붙여 요소를 제거할 수 있지만, 이 방법은 요소를 반복작업하는 동안에만 사용하는 것이 좋습니다. Remove 함수는 제거된 요소 수를 반환하며, 제공된 키가 세트에 들어있지 않은 경우 0이 됩니다. TSet가 중복 키를 지원하는 경우, Remove 함수는 일치하는 모든 요소를 제거합니다.
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는 정렬될 수 있습니다. 정렬 후 세트를 반복작업하면 요소가 정렬된 순서대로 표시되지만, 이 행동은 다음에 세트를 수정할 때까지만 보장됩니다. 정렬이 불안정하므로 중복 키를 지원하는 세트의 동등한 요소는 어떤 순서로든 나타날 수 있습니다.
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를 호출하여 요소를 추가하지 않고 메모리를 할당할 수 있으며, Reset을 호출하거나 0이 아닌 슬랙 파라미터로 Empty를 호출하여 사용 중이던 메모리를 할당 해제하지 않고 요소를 제거할 수 있습니다. 슬랙은 새 메모리를 할당할 필요 없이 미리 할당된 메모리를 사용하여 세트에 새 요소를 추가하는 프로세스를 최적화합니다. 또한, 시스템에서 메모리를 할당 해제할 필요가 없으므로, 요소 제거에도 도움이 될 수 있습니다. 이는 같은 수 이하의 요소로 즉시 다시 채울 것으로 예상되는 세트를 비울 때 특히 효율적입니다.
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 함수는 컨테이너 끝에서 모든 슬랙을 제거하지만, 이 함수는 중간이나 시작 부분의 빈 요소는 그대로 유지됩니다.
// Remove every other element from the set.
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를 제공할 수 있습니다. 키 타입에 대한 KeyFuncs를 생성하려면, 다음과 같이 두 개의 typedef와 세 개의 스태틱 함수를 정의해야 합니다.
KeyInitType— 키를 전달하기 위해 사용하는 타입입니다. 주로 ElementType 템플릿 파라미터에서 끌어옵니다.ElementInitType— 요소를 전달하기 위해 사용하는 타입입니다. 또한, 주로 ElementType 템플릿 파라미터에서 끌어오기 때문에 KeyInitType과 동일합니다.KeyInitType GetSetKey(ElementInitType Element)— 요소의 키를 반환합니다. 세트의 경우, 이는 일반적으로 요소 자체입니다.bool Matches(KeyInitType A, KeyInitType B)—A와B가 같으면true, 그렇지 않으면false를 반환합니다.uint32 GetKeyHash(KeyInitType Key)—Key의 해시 값을 반환합니다.
KeyInitType 및 ElementInitType은 키/요소 타입의 일반적인 전달 규칙을 위한 typedef입니다. 일반적으로 중요하지 않은 타입의 경우 값이 되고, 중요한 타입의 경우 const 레퍼런스가 됩니다. 세트의 요소 타입은 키 타입이기도 하므로 DefaultKeyFuncs는 ElementType라는 템플릿 파라미터 하나만 사용해서 둘을 모두 정의한다는 점을 기억하세요.
TSet는 DefaultKeyFuncs의 Matches를 사용하여 동일성을 비교하는 두 항목이 KeyFuncs의 GetKeyHash에서도 동일한 값을 반환한다고 가정합니다.
이 두 함수 중 하나의 결과에 영향을 주는 방식으로 기존 요소의 키를 수정하면 안 되는데, 이렇게 할 경우 세트의 내부 해시가 무효화되기 때문입니다. 이러한 규칙은 DefaultKeyFuncs 디폴트 구현을 사용할 때 GetKeyHash 및 operator==의 오버로드에도 적용됩니다.
기타
CountBytes 및 GetAllocatedSize 함수는 내부 배열이 현재 사용 중인 메모리 양을 추정합니다. CountBytes는 FArchive 파라미터를 사용하지만, GetAllocatedSize는 그렇지 않습니다. 이러한 함수는 일반적으로 통계 보고에 사용됩니다.
Dump 함수는 FOutputDevice를 받아 세트의 콘텐츠에 대한 일부 구현 정보를 기록합니다. 모든 해시 항목의 모든 요소를 나열하는 DumpHashElements 함수도 있습니다. 이러한 함수는 보통 디버깅에 사용됩니다.