[Unreal] 언리얼 컨테이너 라이브러리
Table of Contents
언리얼 컨테이너 라이브러리 #
- Unreal Container Library (UCL)
- 언리얼이 자체적으로 제작해서 제공하는 라이브러리이다. 언리얼 엔진에 특화되어 있으며, 언리얼 오브젝트 구조만 안정적으로 지원한다. 따라서 언리얼 엔진에서는 C++ STL이 아닌 UCL을 사용하자.
- 다양한 라이브러리가 있지만 실제 게임 제작에 유용하게 사용되는 라이브러리는 다음 세 가지이다.
TArray
,TSet
,TMap
각 라이브러리의 특징 #
- 공통 특징
- 동질성 컨테이너이므로, 안에 있는 요소는 전부 반드시 같은 유형이어야 한다.
int32
,float
와 같은 값 유형이다. 안에 있는 요소도 모두 값 유형이어야 한다.- 소멸되면 그 안의 요소들도 모두 소멸된다.
항목 | TArray |
TSet |
TMap |
TMultiMap |
---|---|---|---|---|
구현 방식 | 동적 배열 | 해시 테이블 | 해시 테이블 | 해시 테이블 |
저장 방식 | 연속 저장 | 이빠진 배열 삽입 시 중간에 빈 공간으로 들어간다. (순서 보장 X) |
이빠진 배열 삽입 시 중간에 빈 공간으로 들어간다. (순서 보장 X) |
이빠진 배열 삽입 시 중간에 빈 공간으로 들어간다. (순서 보장 X) |
접근 방식 | Via Index | Via Key | Via Key | Via Key |
중간 삽입 시 원소 밀어냄 |
O | X | X | X |
중복 가능 | O | X (새 값으로 대체됨) |
X (새 값으로 대체됨) |
O |
자동 정렬 | X | X | X | X |
이럴 때 사용하자! | 고속 순회 랜덤 접근 |
빠른 중복 감지 | 중복 없이 Key, Value로 관리 |
중복 있게 Key, Value로 관리 |
- 시간 복잡도는 다음과 같다.
항목 | TArray |
TSet , TMap , TMultiMap |
---|---|---|
접근 | $O(1)$ | $O(1)$ |
검색 | $O(N)$ | $O(1)$ |
삽입 | $O(N)$ | $O(1)$ |
삭제 | $O(N)$ | $O(1)$ |
TArray
와 슬랙 #
- 매번 재할당하는 것을 피하기 위해서 얼로케이터는 요청한 것보다 넉넉한 메모리를 마련한다.
- 그리고 엘리먼트를 삭제한다고 해서 메모리가 해제되진 않는다. 따라서 배열에 슬랙(slack: 여유분)이 남게 된다.
TArray<int32> SlackArray;
// SlackArray.GetSlack() == 0 // 비어 있는 슬랙 개수
// SlackArray.Num() == 0 // 데이터가 있는 요소 개수
// SlackArray.Max() == 0 // 비어 있는 슬랙 + 데이터가 있는 요소 개수
SlackArray.Add(1);
// SlackArray.GetSlack() == 3
// SlackArray.Num() == 1
// SlackArray.Max() == 4
SlackArray.Add(2);
SlackArray.Add(3);
SlackArray.Add(4);
SlackArray.Add(5);
// SlackArray.GetSlack() == 17
// SlackArray.Num() == 5
// SlackArray.Max() == 22
Empty()
- 슬랙 인수를 받아서 해당 개수만큼 슬랙을 만든다.
SlackArray.Empty();
// SlackArray.GetSlack() == 0
// SlackArray.Num() == 0
// SlackArray.Max() == 0
SlackArray.Empty(3);
// SlackArray.GetSlack() == 3
// SlackArray.Num() == 0
// SlackArray.Max() == 3
SlackArray.Add(1);
SlackArray.Add(2);
SlackArray.Add(3);
// SlackArray.GetSlack() == 0
// SlackArray.Num() == 3
// SlackArray.Max() == 3
Reset()
- 슬랙 인수를 받아서 해당 개수만큼 슬랙을 만드는데,
- 요청된 슬랙이 현재 할당으로 충분한 경우 메모리를 해제하지 않는다.
SlackArray.Reset(0);
// SlackArray.GetSlack() == 3
// SlackArray.Num() == 0
// SlackArray.Max() == 3
SlackArray.Reset(10);
// SlackArray.GetSlack() == 10
// SlackArray.Num() == 0
// SlackArray.Max() == 10
Shrink()
- 모든 슬랙을 제거해서 현재 엘리먼트 저장에 필요한 최소 크기로 할당을 조정한다.
SlackArray.Add(5);
SlackArray.Add(10);
SlackArray.Add(15);
SlackArray.Add(20);
// SlackArray.GetSlack() == 6
// SlackArray.Num() == 4
// SlackArray.Max() == 10
SlackArray.Shrink();
// SlackArray.GetSlack() == 0
// SlackArray.Num() == 4
// SlackArray.Max() == 4
TSet
, TMap
은 이빠진 배열 #
TSet
과TMap
은 이빨이 빠져 있다. 그래서 데이터가 추가되면 그 빈 공간으로 들어가므로, 엘리먼트들의 순서를 보장할 수가 없다.
TSet
에 원소를 넣고 몇 개를 빼보자.
TSet<int32> Int32Set;
for (int32 ix = 1; ix <= ArrayNum; ++ix)
{
Int32Set.Add(ix); // 1 ~ 10을 채워넣는다.
}
// 짝수를 모두 제거한다.
Int32Set.Remove(2);
Int32Set.Remove(4);
Int32Set.Remove(6);
Int32Set.Remove(8);
Int32Set.Remove(10);
- 보여지는 것은 5개의 엘리먼트이지만, 안에 들어가보면 Invalid들이 있다는 것을 알 수 있다. (이빨이 빠져있다)
- 실제 메모리는 처음 할당한 그대로 10개를 차지하고 있다.
- 짝수를 다시 넣으면?
// 짝수를 다시 넣는다.
Int32Set.Add(2);
Int32Set.Add(4);
Int32Set.Add(6);
Int32Set.Add(8);
Int32Set.Add(10);
- 채워 넣을 때는 가장 마지막에 빠진 곳부터 채워진다.
10
을 가장 마지막에 뺐으므로10
이 있던 공간(9번 인덱스)부터 채워진다. (순서를 보장할 수 없다)
TSet
, TMap
에 커스텀 Key를 사용하는 방법 #
- 예를 들어, 커스텀 구조체를 Key로 사용하고 싶다고 하자.
struct FMyStruct
{
// 고유한 Key이다.
FString UniqueID;
// 이것은 중복될 수 있는 값이다. 이건 Key의 고유성을 판별하는 데 필요없는 값이다.
float SomeFloat;
explicit FMyStruct(float InFloat) : UniqueID (FGuid::NewGuid().ToString()), SomeFloat(InFloat)
{
}
};
- 커스텀 구조체는 고유성을 판별하기 위한
opreator==()
와GetTypeHash()
가 없으므로, 컴파일 에러가 발생한다. 따라서 추가해주어야 Key로 사용할 수 있다.
struct FMyStruct
{
//...
// (1)
bool operator==(const FMyStruct& InOther) const
{
return UniqueID == InOther.UniqueID;
}
// (2)
friend FORCEINLINE uint32 GetTypeHash(const FMyStruct& InData)
{
return GetTypeHash(InData.UniqueID);
}
};
- 하지만 이런 함수를 만드는 것이 바람직하지 않은 경우에는 별도의 커스텀 KeyFuncs 를 제공하면 된다.
template <typename ValueType>
struct TMyStructMapKeyFuncs : BaseKeyFuncs<TPair<FMyStruct, ValueType>, FString>
{
private:
typedef BaseKeyFuncs<TPair<FMyStruct, ValueType>, FString> Super;
public:
// (1) 키 전달에 사용
typedef typename Super::KeyInitType KeyInitType;
// (2) 엘리먼트 전달에 사용
typedef typename Super::ElementInitType ElementInitType;
// (3) 엘리먼트의 키를 반환
static KeyInitType GetSetKey(ElementInitType Element)
{
return Element.Key.UniqueID;
}
// (4) A 와 B 가 동일하면 true, 아니면 false 를 반환
static bool Matches(KeyInitType A, KeyInitType B)
{
return A.Compare(B, ESearchCase::CaseSensitive) == 0;
}
// (5) 키의 해시 값을 반환
static uint32 GetKeyHash(KeyInitType Key)
{
return FCrc::StrCrc32(*Key);
}
};
STL과의 차이점 #
TArray
의 경우 STL의vector
와 동작 원리가 유사하다.
- 반면
TSet
,TMap
은 STL의set
,map
과 차이점이 있다.
항목 | set , map |
TSet , TMap |
---|---|---|
구현 방식 | 이진 트리 | 해시 테이블 |
저장 방식 | 불연속 저장 순회에 적합하지 않음 |
동적 배열 형태 (모여 있다) 빠르게 순회 가능 |
중복 가능 | X (첫 값만 유지) |
X (새 값으로 대체됨) |
자동 정렬 | O (삭제 시 재구축이 일어남) |
X (재구축 없음) |
비어 있는 데이터 | 없음 | 있을 수 있음 |