TSet

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

Windows
MacOS
Linux
추가 참고

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 가 생성됩니다. TSetoperator== 로 엘리먼트를 직접 비교하고, 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 편집

TSetUPROPERTY() 매크로와 편집가능 키워드 (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"

CreateIteratorCreateConstIterators 함수로 이터레이터를 만들 수도 있습니다. CreateIterator 는 읽기-쓰기 가능한 이터레이터를 반환하는 반면, CreateConstIterator 는 읽기 전용 이터레이터를 반환합니다. 어느 경우든, 이 이터레이터의 KeyValue 함수를 사용해서 엘리먼트를 조사할 수 있습니다. 우리 예제의 "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 == []

EmptyReset 은 비슷하지만, 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 이 아닌 슬랙 파라미터로 호출하면 됩니다. 메모리 할당 해제할 필요가 없으니 엘리먼트 제거에도 도움이 됩니다. 특히 세트를 비우고 엘리먼트 수가 같거나 적은 세트를 바로 다시 채우려는 경우 특히 효율적입니다.

TSetTArrayMax 함수처럼 미리 할당된 엘리먼트 수를 검사하는 방법이 제공되지 않습니다.

다음 코드는 메모리 할당 해제 없이 세트에서 모든 엘리먼트를 제거하여 슬랙을 만듭니다:

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 에서 모든 슬랙을 제거하려면, CollapseShrink 함수를 사용하세요. 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 함수를 호출합니다.

KeyInitTypeElementInitType 은 키/엘리먼트 유형의 일반 전달 규칙에 대한 typedef 입니다. 보통 이들은 사소한(trivial) 유형에 대해서는 값이, 사소하지 않은 유형에 대해서는 const 레퍼런스가 됩니다. 세트의 엘리먼트 유형은 키 유형이기도 하다는 점, 그래서 DefaultKeyFuncs 가 ElementType 라는 템플릿 파라미터 하나만 사용해서 둘을 정의하고 있다는 점 기억해 주세요.

TSet 는 (DefaultKeyFuncs 의) Matches 를 사용하여 동일성 비교를 하는 두 항목이 (KeyFuncs 의) GetKeyHash 에서도 같은 값을 반환한다 가정합니다.

이 두 함수 중 하나의 결과에 영향을 주는 방식으로 기존 엘리먼트의 키를 변경하면 안되는데, TSet 의 내부 해시를 무효화시키기 때문입니다. 이 규칙은 DefaultKeyFuncs 기본 구현 사용시 GetKeyHashoperator== 의 오버로드에도 적용됩니다.

기타

CountBytesGetAllocatedSize 함수는 현재 내부 배열에 활용되고 있는 메모리 양을 측정합니다. CountBytesFArchive 파라미터를 받는 반면, GetAllocatedSize 는 받지 않습니다. 이 함수는 일반적으로 통계 보고에 사용됩니다.

Dump 함수는 FOutputDevice 를 받아 세트의 내용 관련 약간의 구현 정보를 출력합니다. DumpHashElements 라는, 모든 해시 항목에서 모든 엘리먼트를 나열하는 함수도 있습니다. 이 함수들은 보통 디버깅에 사용됩니다.

Select Skin
Light
Dark

새로운 언리얼 엔진 4 문서 사이트에 오신 것을 환영합니다!

문서 사이트에 대한 의견을 모을 수 있는 피드백 시스템을 포함해서 여러가지 새로운 기능을 준비하고 있습니다. 아래 Documentation Feedback 포럼(영문) 또는 언리얼 엔진 네이버 공식 카페(한글) 중 편하신 곳에 의견이나 문제점을 알려 주세요.

새 시스템이 준비되면 알려 드리겠습니다.

네이버 카페
공식 포럼