언리얼 엔진에서 가장 간단한 컨테이너 클래스는 TArray입니다. TArray는 타입이 같은 다른 오브젝트('요소'라고 함) 시퀀스의 소유권 및 정리를 담당합니다. TArray는 시퀀스이므로 그 요소는 잘 정리된 순서를 가지며, 해당 함수는 이러한 오브젝트와 순서를 결정론적으로 조작하는 데 사용됩니다.
TArray
TArray는 언리얼 엔진에서 가장 일반적인 컨테이너 클래스입니다. 빠르고, 메모리 효율적이며, 안전합니다. TArray 타입은 요소 타입과 선택적 얼로케이터라는 두 가지 프로퍼티로 정의됩니다.
요소 타입은 배열에 저장될 오브젝트의 타입입니다. TArray는 동질성 컨테이너라고 불리며, 이는 모든 요소가 엄격하게 동일한 타입이라는 뜻입니다. 즉, 다른 타입의 요소를 하나의 TArray에 저장할 수는 없습니다.
얼로케이터는 생략되는 경우가 많으며, 대부분의 사용 사례에 적합한 얼로케이터가 디폴트로 지정됩니다. 이는 오브젝트가 메모리에 배치되는 방식과 더 많은 요소를 수용하기 위해 배열이 확장되는 방법을 정의합니다. 디폴트 행동이 적합하지 않다고 판단되는 경우 사용할 수 있는 다양한 얼로케이터가 있으며, 얼로케이터를 직접 작성할 수도 있습니다. 이에 관한 자세한 내용은 나중에 다루겠습니다.
TArray는 값 타입이므로 int32나 float와 같은 다른 내장 타입과 비슷하게 취급해야 합니다. 확장을 염두에 두고 디자인되지는 않았기에, new 및 delete를 통해 TArray 인스턴스를 생성하거나 삭제하는 것은 권장하지 않습니다. 요소도 값 타입이며 그 배열이 이를 소유합니다. TArray가 소멸하면 그 안에 남아있는 모든 요소가 소멸합니다. 다른 변수에서 TArray 변수를 생성하면 그 요소가 새 변수로 복사되며, 공유되는 상태는 없습니다.
배열 생성 및 채우기
배열을 생성하려면 다음과 같이 정의합니다.
TArray<int32> IntArray;그러면 integer의 시퀀스를 보유하도록 디자인된 빈 배열이 생성됩니다. 요소 타입은 int32, FString, TSharedPtr 등, 일반적인 C++ 값 규칙에 따라 복사할 수 있고 소멸 가능한 모든 값 타입이 될 수 있습니다. 얼로케이터가 지정되지 않았으므로 TArray는 디폴트 힙 기반 얼로케이터를 사용합니다. 이 시점에서는 아직 할당된 메모리가 없습니다.
여러 언리얼 엔진 컨테이너와 마찬가지로 TArray는 요소 타입이 평범하게 재배치할 수 있다고 가정하는데, 이는 원시 바이트를 직접 복사하여 메모리 내 한 위치에서 다른 위치로 요소를 안전하게 이동할 수 있다는 뜻입니다.
TArray는 여러 방법으로 채울 수 있습니다. 그중 하나는 배열을 여러 요소의 사본으로 채우는 Init 함수를 사용하는 것입니다.
IntArray.Init(10, 5);
// IntArray == [10,10,10,10,10]Add 및 Emplace 함수를 사용하면 배열 끝에 새 요소를 생성할 수 있습니다.
TArray<FString> StrArr;
StrArr.Add (TEXT("Hello"));
StrArr.Emplace(TEXT("World"));
// StrArr == ["Hello","World"]배열의 얼로케이터는 배열에 새 요소가 추가될 때 필요에 따라 메모리를 제공합니다. 디폴트 얼로케이터는 현재 배열 크기가 초과될 때마다 여러 개의 새 요소에 충분한 메모리를 추가합니다. Add 및 Emplace는 거의 동일한 기능을 수행하지만, 다음과 같이 미묘한 차이가 있습니다.
Add(또는Push)는 요소 타입의 인스턴스를 배열에 복사(또는 이동)합니다.Emplace는 사용자가 제공한 실행인자를 사용하여 요소 타입의 새 인스턴스를 생성합니다.
여기 TArray<FString>의 경우, Add는 스트링 리터럴에서 임시 FString을 생성한 다음, 그 임시 FString의 콘텐츠를 컨테이너 내부의 새 FString으로 이동하지만, Emplace는 스트링 리터럴을 사용하여 직접 새 FString을 생성합니다. 최종 결과는 동일하지만, Emplace는 임시 변수를 생성하지 않으므로 FString 같은 무시할 수 없는 값 타입에는 바람직하지 않은 경우가 많습니다.
일반적으로 호출 사이트에서 불필요한 임시 변수를 만든 다음 컨테이너로 복사하거나 이동하지 않는다는 점에서 Emplace가 Add보다 바람직합니다. 보통 중요하지 않은 타입은 Add를 사용하고 그렇지 않은 경우에는 Emplace를 사용합니다. Emplace가 Add보다 효율이 떨어지는 것은 아니지만, 가독성은 Add가 나을 수도 있습니다.
Append는 다른 TArray 또는 일반 C 배열에 대한 포인터와 해당 배열의 크기에서 한 번에 여러 요소를 추가합니다.
FString Arr[] = { TEXT("of"), TEXT("Tomorrow") };
StrArr.Append(Arr, ARRAY_COUNT(Arr));
// StrArr == ["Hello","World","of","Tomorrow"]AddUnique는 동일한 요소가 아직 없는 경우에만 컨테이너에 새 요소를 추가합니다. 요소 타입의 operator==를 사용하여 동일성을 확인합니다.
StrArr.AddUnique(TEXT("!"));
// StrArr == ["Hello","World","of","Tomorrow","!"]
StrArr.AddUnique(TEXT("!"));
// StrArr is unchanged as "!" is already an elementInsert는 Add, Emplace 및 Append와 마찬가지로 지정된 인덱스에 단일 요소나 요소 배열의 사본을 추가합니다.
StrArr.Insert(TEXT("Brave"), 1);
// StrArr == ["Hello","Brave","World","of","Tomorrow","!"]SetNum 함수는 배열 요소의 숫자를 직접 설정할 수 있으며, 새 숫자가 현재 숫자보다 크면 요소 타입의 디폴트 생성자를 사용하여 새 요소를 생성합니다.
StrArr.SetNum(8);
// StrArr == ["Hello","Brave","World","of","Tomorrow","!","",""]새 숫자가 현재 숫자보다 작으면 SetNum은 요소도 제거합니다. 요소 제거에 관한 자세한 내용은 추후 제공할 예정입니다.
StrArr.SetNum(6);
// StrArr == ["Hello","Brave","World","of","Tomorrow","!"]반복작업
배열의 요소를 반복작업하는 방법에는 여러 가지가 있지만, C++ 범위 기반 for 문을 사용하는 것을 권장합니다.
FString JoinedStr;
for (auto& Str : StrArr)
{
JoinedStr += Str;
JoinedStr += TEXT(" ");
}
// JoinedStr == "Hello Brave World of Tomorrow ! "물론 일반 인덱스 기반 반복작업도 가능합니다.
for (int32 Index = 0; Index != StrArr.Num(); ++Index)
{
JoinedStr += StrArr[Index];
JoinedStr += TEXT(" ");
}마지막으로 배열에는 자체 이터레이터 타입이 있어 반복작업을 더 잘 제어할 수 있습니다. CreateIterator와 CreateConstIterator라는 두 가지 함수가 있는데, 각각 요소에 대한 읽기-쓰기 액세스나 읽기 전용 액세스에 사용할 수 있습니다.
for (auto It = StrArr.CreateConstIterator(); It; ++It)
{
JoinedStr += *It;
JoinedStr += TEXT(" ");
}정렬
배열은 Sort 함수를 호출하여 간단하게 정렬할 수 있습니다.
StrArr.Sort();
// StrArr == ["!","Brave","Hello","of","Tomorrow","World"]여기서의 값은 요소 타입의 operator<를 사용하여 정렬됩니다. FString의 경우, 대소문자를 구분하지 않고 사전적으로 비교합니다. 바이너리 술어는 다음과 같이 다양한 순서 지정 의미론을 제공하도록 구현할 수도 있습니다.
StrArr.Sort([](const FString& A, const FString& B) {
return A.Len() < B.Len();
});
// StrArr == ["!","of","Hello","Brave","World","Tomorrow"]이제 스트링이 길이별로 정렬됩니다. 길이가 같은 세 개의 스트링인 'Hello', 'Brave', 'World'가 배열 내 이전 위치를 기준으로 순서가 어떻게 바뀌었는지 잘 살펴보세요. 이는 Sort가 불안정하고 동등한 요소의 상대적 순서(여기서는 술어가 길이만 비교하므로 해당 스트링이 동등함)가 보장되지 않기 때문입니다. Sort는 퀵 정렬로 구현됩니다.
바이너리 술어의 여부와 무관하게 HeapSort 함수는 힙 정렬을 수행하는 데 사용할 수 있습니다. 이 함수의 사용 여부는 특정한 데이터, 그리고 이 함수가 Sort 함수보다 얼마나 효율적으로 정렬할 수 있는지에 따라 달라집니다. Sort와 마찬가지로 HeapSort는 안정적이지 않습니다. 위에서 Sort 대신 HeapSort를 사용했다면, 결과는 다음과 같을 것입니다(이 경우 동일).
StrArr.HeapSort([](const FString& A, const FString& B) {
return A.Len() < B.Len();
});
// StrArr == ["!","of","Hello","Brave","World","Tomorrow"]마지막으로 StableSort를 사용하여 정렬 후 동등한 요소의 상대적 순서를 보장할 수 있습니다. 위에서 Sort 또는 HeapSort 대신 StableSort를 호출했다면 다음과 같은 결과가 나왔을 것입니다.
StrArr.StableSort([](const FString& A, const FString& B) {
return A.Len() < B.Len();
});
// StrArr == ["!","of","Brave","Hello","World","Tomorrow"]즉, 'Hello', 'Brave', 'World'는 이전에 사전적으로 정렬된 후에도 동일한 상대적 순서를 유지합니다. StableSort는 병합 정렬로 구현됩니다.
쿼리
Num 함수를 사용하여 배열에 몇 개의 요소가 있는지 물어볼 수 있습니다.
int32 Count = StrArr.Num();
// Count == 6C 스타일 API와의 상호 운용성이 필요한 경우처럼 배열 메모리에 직접 액세스해야 하는 경우, GetData 함수를 사용하여 배열의 요소에 대한 포인터를 반환할 수 있습니다. 이 포인터는 배열이 존재하는 동안, 그리고 배열에 대한 변경되기 전에만 유효합니다. StrPtr의 첫 번째 Num 인덱스만 역참조할 수 있습니다.
FString* StrPtr = StrArr.GetData();
// StrPtr[0] == "!"
// StrPtr[1] == "of"
// ...
// StrPtr[5] == "Tomorrow"
// StrPtr[6] - undefined behavior컨테이너가 const면 반환된 포인터도 const가 됩니다.
컨테이너에 요소의 크기를 물어볼 수도 있습니다.
uint32 ElementSize = StrArr.GetTypeSize();
// ElementSize == sizeof(FString)요소를 얻으려면 인덱싱 operator[]를 사용하여 원하는 요소에 0부터 시작하는 인덱스를 전달하면 됩니다.
FString Elem1 = StrArr[1];
// Elem1 == "of"유효하지 않은 인덱스(0보다 작거나 Num() 이상인 인덱스)를 전달하면 런타임 오류가 발생합니다. 특정 인덱스가 유효한지 컨테이너에 IsValidIndex 함수를 사용하여 물어볼 수 있습니다.
bool bValidM1 = StrArr.IsValidIndex(-1);
bool bValid0 = StrArr.IsValidIndex(0);
bool bValid5 = StrArr.IsValidIndex(5);
bool bValid6 = StrArr.IsValidIndex(6);
// bValidM1 == false
// bValid0 == true
// bValid5 == true
// bValid6 == falseoperator[]는 레퍼런스를 반환하므로 배열이 const가 아니면 배열 내부의 요소를 변경하는 데에도 사용할 수 있습니다.
StrArr[3] = StrArr[3].ToUpper();
// StrArr == ["!","of","Brave","HELLO","World","Tomorrow"]GetData 함수와 마찬가지로 operator[]도 배열이 const이면 const 레퍼런스를 반환합니다. Last 함수를 사용하여 배열의 끝에서 거꾸로 인덱싱할 수도 있습니다. 인덱스의 디폴트 값은 0입니다. Top 함수는 Last와 같지만, 인덱스를 받지 않는다는 점이 다릅니다.
FString ElemEnd = StrArr.Last();
FString ElemEnd0 = StrArr.Last(0);
FString ElemEnd1 = StrArr.Last(1);
FString ElemTop = StrArr.Top();
// ElemEnd == "Tomorrow"
// ElemEnd0 == "Tomorrow"
// ElemEnd1 == "World"
// ElemTop == "Tomorrow"다음과 같이 배열에 특정 요소가 포함되어 있는지 물어볼 수 있습니다.
bool bHello = StrArr.Contains(TEXT("Hello"));
bool bGoodbye = StrArr.Contains(TEXT("Goodbye"));
// bHello == true
// bGoodbye == false또는 배열에 특정 술어와 일치하는 요소가 포함되어 있는지 물어볼 수 있습니다.
bool bLen5 = StrArr.ContainsByPredicate([](const FString& Str){
return Str.Len() == 5;
});
bool bLen6 = StrArr.ContainsByPredicate([](const FString& Str){
return Str.Len() == 6;
});
// bLen5 == true
// bLen6 == falseFind 함수군을 사용하여 요소를 찾을 수 있습니다. 요소의 존재를 확인하고 해당 인덱스를 반환하려면 Find를 사용합니다.
int32 Index;
if (StrArr.Find(TEXT("Hello"), Index))
{
// Index == 3
}이렇게 하면 Index가 처음 찾은 요소의 인덱스로 설정됩니다. 중복된 요소가 있고 대신 마지막 요소의 인덱스를 찾으려는 경우, FindLast 함수를 대신 사용합니다.
int32 IndexLast;
if (StrArr.FindLast(TEXT("Hello"), IndexLast))
{
// IndexLast == 3, because there aren't any duplicates
}이 두 함수 모두 요소를 찾았는지 여부를 나타내는 bool을 반환하고, 요소를 찾으면 해당 요소의 인덱스를 변수에 씁니다.
Find 및 FindLast는 요소 인덱스를 직접 반환할 수도 있습니다. 인덱스를 명시적 실행인자로 전달하지 않으면 해당 작업을 수행합니다. 이는 위의 함수보다 간결할 수 있으며, 어떤 함수를 사용할지는 특정 요구 사항이나 스타일에 따라 달라집니다.
요소를 찾지 못하면 특수한 INDEX_NONE 값이 반환됩니다.
int32 Index2 = StrArr.Find(TEXT("Hello"));
int32 IndexLast2 = StrArr.FindLast(TEXT("Hello"));
int32 IndexNone = StrArr.Find(TEXT("None"));
// Index2 == 3
// IndexLast2 == 3
// IndexNone == INDEX_NONEIndexOfByKey는 비슷하게 작동하지만, 이를 사용하여 요소를 임의의 오브젝트와 비교할 수 있습니다. Find 함수를 사용하면 검색이 시작되기 전에 실행인자가 실제로 요소 타입(이 경우 FString)으로 변환됩니다. IndexOfByKey를 사용하면 키를 직접 비교하므로 키 타입을 요소 타입으로 직접 변환할 수 없는 경우에도 검색을 지원합니다.
IndexOfByKey는 operator==(ElementType, KeyType)이 존재하는 모든 키 타입에 대해 동작합니다. IndexOfByKey는 처음 찾은 요소의 인덱스를 반환하고, 요소를 찾지 못한 경우 INDEX_NONE을 반환합니다.
int32 Index = StrArr.IndexOfByKey(TEXT("Hello"));
// Index == 3IndexOfByPredicate 함수는 지정된 술어와 일치하는 첫 번째 요소의 인덱스를 찾고, 인덱스를 찾을 수 없으면, 다시 특수한 INDEX_NONE 값을 반환합니다.
int32 Index = StrArr.IndexOfByPredicate([](const FString& Str){
return Str.Contains(TEXT("r"));
});
// Index == 2인덱스를 반환하는 대신 찾은 요소에 대한 포인터를 반환할 수 있습니다. FindByKey는 요소를 임의의 오브젝트와 비교하지만, 찾은 요소에 대한 포인터를 반환하는 IndexOfByKey처럼 작동합니다. 요소를 찾지 못하면 nullptr을 반환합니다.
auto* OfPtr = StrArr.FindByKey(TEXT("of")));
auto* ThePtr = StrArr.FindByKey(TEXT("the")));
// OfPtr == &StrArr[1]
// ThePtr == nullptr인덱스 대신 포인터를 반환한다는 점을 제외하면 FindByPredicate는 IndexOfByPredicate처럼 사용할 수 있습니다.
auto* Len5Ptr = StrArr.FindByPredicate([](const FString& Str){
return Str.Len() == 5;
});
auto* Len6Ptr = StrArr.FindByPredicate([](const FString& Str){
return Str.Len() == 6;
});
// Len5Ptr == &StrArr[2]
// Len6Ptr == nullptr마지막으로 FilterByPredicate 함수를 사용하여 특정 술어와 일치하는 요소 배열을 얻을 수 있습니다.
auto Filter = StrArray.FilterByPredicate([](const FString& Str){
return !Str.IsEmpty() && Str[0] < TEXT('M');
});제거
Remove 함수군을 사용하여 배열에서 요소를 지울 수 있습니다. Remove 함수는 요소 타입의 operator== 함수에 따라 제공한 요소와 동일한 것으로 간주되는 모든 요소를 제거합니다. 예를 들어 다음과 같이 할 수 있습니다.
TArray<int32> ValArr;
int32 Temp[] = { 10, 20, 30, 5, 10, 15, 20, 25, 30 };
ValArr.Append(Temp, ARRAY_COUNT(Temp));
// ValArr == [10,20,30,5,10,15,20,25,30]
ValArr.Remove(20);
// ValArr == [10,30,5,10,15,25,30]RemoveSingle을 사용하여 배열에서 처음 일치한 요소를 지울 수도 있습니다. 배열에 중복된 요소가 있을 수 있는데 하나만 지우려는 경우, 또는 배열에 일치하는 요소가 하나만 포함될 수 있다는 것을 알고 있는 경우 최적화에 이 기능을 다음과 같이 유용하게 사용할 수 있습니다.
ValArr.RemoveSingle(30);
// ValArr == [10,5,10,15,25,30]RemoveAt 함수를 사용하여 0부터 시작하는 인덱스를 기준으로 요소를 제거할 수도 있습니다. 이 함수에 유효하지 않은 인덱스를 전달하면 런타임 오류가 발생할 수 있으므로 IsValidIndex를 사용하여 제공하려는 인덱스가 있는 요소가 배열에 있는지 검증하는 것이 좋습니다.
ValArr.RemoveAt(2); // Removes the element at index 2
// ValArr == [10,5,15,25,30]
ValArr.RemoveAt(99); // This will cause a runtime error as
// there is no element at index 99또한, RemoveAll 함수를 사용하여 술어와 일치하는 요소를 제거할 수도 있습니다. 예를 들어, 다음 예시에서는 3의 배수인 모든 값을 제거합니다.
ValArr.RemoveAll([](int32 Val) {
return Val % 3 == 0;
});
// ValArr == [10,5,25]이 모든 경우에서 요소가 제거될 때 뒤에 오는 요소는 더 낮은 인덱스로 셔플되어 내려갑니다. 배열에 구멍이 있으면 안 되기 때문입니다.
셔플링 프로세스에는 오버헤드가 있습니다. 나머지 요소의 순서가 중요하지 않다면, RemoveSwap, RemoveAtSwap 및 RemoveAllSwap 함수를 사용하여 이러한 오버헤드를 줄일 수 있습니다. 이 함수들은 나머지 요소의 순서를 보장하지 않는다는 점을 제외하면 교체가 없는 베리언트와 동일하게 작동하여 작업을 더 빨리 완료할 수 있기 때문입니다.
TArray<int32> ValArr2;
for (int32 i = 0; i != 10; ++i)
ValArr2.Add(i % 5);
// ValArr2 == [0,1,2,3,4,0,1,2,3,4]
ValArr2.RemoveSwap(2);
// ValArr2 == [0,1,4,3,4,0,1,3]
ValArr2.RemoveAtSwap(1);
// ValArr2 == [0,3,4,3,4,0,1]
마지막으로 Empty 함수는 배열에서 모든 것을 제거합니다.
ValArr2.Empty();
// ValArr2 == []연산자
배열은 일반 값 타입이므로 표준 복사 생성자나 할당 연산자로 복사할 수 있습니다. 배열은 엄격하게 요소를 소유하므로 배열은 깊게 복사됩니다. 따라서 새 배열은 자체적인 요소 사본을 갖게 됩니다.
TArray<int32> ValArr3;
ValArr3.Add(1);
ValArr3.Add(2);
ValArr3.Add(3);
auto ValArr4 = ValArr3;
// ValArr4 == [1,2,3];
ValArr4[0] = 5;
// ValArr3 == [1,2,3];
// ValArr4 == [5,2,3];Append 함수 대신 operator+=를 사용하여 배열을 연결할 수 있습니다.
ValArr4 += ValArr3;
// ValArr4 == [5,2,3,1,2,3]TArray는 MoveTemp 함수를 사용하여 호출할 수 있는 이동 시맨틱도 지원합니다. 이동 후에는 소스 배열이 확실히 비워집니다.
ValArr3 = MoveTemp(ValArr4);
// ValArr3 == [5,2,3,1,2,3]
// ValArr4 == []operator== 및 operator!=를 사용하여 배열을 비교할 수 있습니다. 중요한 것은 요소의 순서입니다. 두 배열은 같은 수의 요소가 같은 순서로 배열되어 있을 때만 동일합니다. 요소는 자체 operator==를 사용하여 비교됩니다.
TArray<FString> FlavorArr1;
FlavorArr1.Emplace(TEXT("Chocolate"));
FlavorArr1.Emplace(TEXT("Vanilla"));
// FlavorArr1 == ["Chocolate","Vanilla"]
auto FlavorArr2 = Str1Array;
// FlavorArr2 == ["Chocolate","Vanilla"]
bool bComparison1 = FlavorArr1 == FlavorArr2;
// bComparison1 == true
힙(Heap)
TArray에는 바이너리 힙 데이터 구조체를 지원하는 함수가 있습니다. 힙은 바이너리 트리의 한 타입으로, 부모 노드가 모든 자손 노드와 동등하거나 그보다 먼저 정렬되는 것을 말합니다. 배열로 구현할 경우, 바이너리 트리의 루트 노드는 요소 0에 있고 인덱스 N에 있는 노드의 왼쪽과 오른쪽 자손 노드의 인덱스 N은 각각 2N+1과 2N+2입니다. 자손들은 서로에 대해 특별한 순서가 정해져 있지 않습니다.
기존 배열은 Heapify 함수를 호출하여 힙으로 전환할 수 있습니다. 이 함수는 술어를 받거나 받지 않도록 오버로드되어 있으며, 술어가 없는 버전은 요소 타입의 operator<를 사용하여 순서를 결정합니다.
TArray<int32> HeapArr;
for (int32 Val = 10; Val != 0; --Val)
{
HeapArr.Add(Val);
}
// HeapArr == [10,9,8,7,6,5,4,3,2,1]
HeapArr.Heapify();
// HeapArr == [1,2,4,3,6,5,8,10,7,9]다음은 그 트리를 시각화한 것입니다.
트리의 노드는 힙화된 배열의 요소 순서대로 왼쪽에서 오른쪽, 위에서 아래로 읽을 수 있습니다. 배열이 힙으로 변환된 후 반드시 정렬되는 것은 아니라는 점에 유의하세요. 정렬된 배열도 유효한 힙이 될 수 있지만, 힙 구조체 정의는 동일한 요소 세트에 대해 여러 개의 유효한 힙을 허용할 만큼 느슨하게 정의되어 있습니다.
HeapPush 함수로 새로운 요소를 힙에 추가하고 다른 노드의 순서를 다음과 같이 변경하여 힙을 유지할 수 있습니다.
HeapArr.HeapPush(4);
// HeapArr == [1,2,4,3,4,5,8,10,7,9,6]HeapPop 및 HeapPopDiscard 함수는 힙에서 최상위 노드를 제거하는 데 사용됩니다. 전자는 요소 타입에 대한 레퍼런스를 받아 최상위 요소의 사본을 반환하고, 후자는 어떤 방식으로도 반환하지 않고 단순히 최상위 노드를 제거한다는 점에서 차이가 있습니다. 두 함수 모두 배열을 동일하게 변경하며, 다른 요소의 순서를 적절히 변경하여 힙을 다시 유지합니다.
int32 TopNode;
HeapArr.HeapPop(TopNode);
// TopNode == 1
// HeapArr == [2,3,4,6,4,5,8,10,7,9]HeapRemoveAt은 주어진 인덱스의 요소를 배열에서 제거한 다음, 요소 순서를 변경하여 힙을 유지합니다.
HeapArr.HeapRemoveAt(1);
// HeapArr == [2,4,4,6,9,5,8,10,7]HeapPush, HeapPop, HeapPopDiscard 및 HeapRemoveAt은 구조체가 이미 유효한 힙인 경우에만 호출해야 합니다. 예를 들어, Heapify 호출 이후, 다른 힙 연산 이후, 또는 배열을 힙으로 수동 조작하여 호출해야 합니다.
Heapify를 포함하여 이러한 각 함수는 옵션으로 바이너리 술어를 받아 힙에 있는 노드 요소의 순서를 결정할 수 있습니다. 기본적으로 힙 연산은 요소 타입의 operator<를 사용하여 순서를 결정합니다. 커스텀 술어를 사용할 때는 모든 힙 연산에 동일한 술어를 사용하는 것이 중요합니다.
마지막으로 배열을 변경하지 않고도 HeapTop을 사용하여 힙의 최상위 노드를 검사할 수 있습니다.
int32 Top = HeapArr.HeapTop();
// Top == 2Slack
배열은 크기를 조정할 수 있으므로 메모리의 양을 가변적으로 사용합니다. 요소가 추가될 때마다 재할당을 방지하기 위해 얼로케이터는 일반적으로 요청한 것보다 많은 메모리를 제공하여 향후 Add 호출이 재할당 퍼포먼스 페널티를 지불하지 않도록 합니다. 마찬가지로 요소를 제거해도 일반적으로 메모리가 확보되지 않습니다. 이렇게 하면 배열에 슬랙 요소가 남게 되는데, 이는 현재 사용 중이지 않은, 사실상 미리 할당된 요소 스토리지 슬롯입니다. 배열의 슬랙 양은 배열에 저장된 요소 수와 배열이 할당된 메모리 양으로 저장할 수 있는 요소 수 사이의 차이로 정의됩니다.
디폴트로 생성된 배열은 메모리를 할당하지 않으므로 처음에는 슬랙이 0입니다. 배열에 얼마나 많은 슬랙이 있는지는 GetSlack 함수를 사용하여 확인할 수 있습니다. 컨테이너가 재할당되기 전에 배열이 보유할 수 있는 최대 요소 수는 Max 함수로 구할 수 있습니다. GetSlack은 Max와 Num 간의 차이와 같습니다.
TArray<int32> SlackArray;
// SlackArray.GetSlack() == 0
// SlackArray.Num() == 0
// SlackArray.Max() == 0
SlackArray.Add(1);
// SlackArray.GetSlack() == 3
// SlackArray.Num() == 1
// SlackArray.Max() == 4
재할당 후 컨테이너의 슬랙 양은 얼로케이터가 결정하므로 사용자는 슬랙이 일정하게 유지되리라고 가정하면 안 됩니다.
슬랙 관리는 필수는 아니지만, 배열 최적화 힌트를 얻는 데 사용할 수 있습니다. 예를 들어, 배열에 100개의 새 요소를 추가할 예정이라는 걸 안다면, 추가하기 전에 100개 이상의 슬랙을 확보하면 배열이 새 요소를 추가하는 동안 메모리를 할당할 필요가 없게 됩니다. 위에서 언급한 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);
Empty와 비슷하게 작동하는 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 함수를 사용하여 모든 슬랙을 제거할 수 있습니다. 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
원시 메모리
TArray는 결국 할당된 메모리를 둘러싼 래퍼에 불과합니다. TArray를 그런 래퍼로 취급하여 할당 바이트를 직접 수정하거나 요소를 직접 생성하는 식으로 다루는 것이 유용할 수도 있습니다. TArray는 항상 보유한 정보로 최선을 다하지만, 때로는 더 낮은 레벨로 내려가야 할 수도 있습니다.
다음 함수는 TArray와 그 안에 저장된 데이터에 대해 빠른 로우 레벨 액세스를 제공하지만, 잘못 사용하면 컨테이너를 유효하지 않은 상태로 만들어 정의되지 않은 행동이 발생할 수 있습니다. 이러한 함수를 호출한 후 다른 일반 함수가 호출되기 전에 컨테이너를 유효한 상태로 반환하는 것은 사용자의 책임입니다.
AddUninitialized 및 InsertUninitialized 함수는 배열에 초기화되지 않은 스페이스를 추가합니다. 각각 Add 및 Insert 함수처럼 작동하지만, 요소 타입의 생성자를 호출하지는 않습니다. 이는 다음 예시처럼 생성자 호출을 방지하는 데 유용할 수 있습니다. 다음 예시와 같이 전체 구조체를 Memcpy 호출로 덮어쓰려는 경우 이 함수를 사용할 수 있습니다.
int32 SrcInts[] = { 2, 3, 5, 7 };
TArray<int32> UninitInts;
UninitInts.AddUninitialized(4);
FMemory::Memcpy(UninitInts.GetData(), SrcInts, 4*sizeof(int32));
// UninitInts == [2,3,5,7]이 기능을 사용하여 직접 생성하려는 오브젝트를 위한 메모리를 예약할 수도 있습니다.
TArray<FString> UninitStrs;
UninitStrs.Emplace(TEXT("A"));
UninitStrs.Emplace(TEXT("D"));
UninitStrs.InsertUninitialized(1, 2);
new ((void*)(UninitStrs.GetData() + 1)) FString(TEXT("B"));
new ((void*)(UninitStrs.GetData() + 2)) FString(TEXT("C"));
// UninitStrs == ["A","B","C","D"]추가된 스페이스와 삽입된 스페이스의 바이트도 0으로 만든다는 점을 제외하면 AddZeroed와 InsertZeroed는 비슷하게 작동합니다.
struct S
{
S(int32 InInt, void* InPtr, float InFlt)
: Int(InInt)
, Ptr(InPtr)
, Flt(InFlt)
{
}
int32 Int;
void* Ptr;
새 숫자가 현재 숫자보다 큰 경우 새 요소의 스페이스가 각각 초기화되지 않은 상태로 남거나 비트 단위로 0이 된다는 점을 제외하면 SetNum과 같은 방식으로 작동하는 SetNumUninitialized 및 SetNumZeroed 함수도 있습니다. AddUninitialized 및 InsertUninitialized 함수와 마찬가지로, 필요한 경우 새 요소가 새 스페이스에 적절하게 생성되도록 보장해야 합니다.
SArr.SetNumUninitialized(3);
new ((void*)(SArr.GetData() + 1)) S(5, (void*)0x12345678, 3.14);
new ((void*)(SArr.GetData() + 2)) S(2, (void*)0x87654321, 2.72);
// SArr == [
// { Int: 0, Ptr: nullptr, Flt: 0.0f },
// { Int: 5, Ptr: 0x12345678, Flt: 3.14f },
// { Int: 2, Ptr: 0x87654321, Flt: 2.72f }
// ]
SArr.SetNumZeroed(5);
'Uninitialized' 및 'Zeroed' 함수군은 신중하게 사용해야 합니다. 요소 타입에 생성이 필요한 멤버가 포함되어 있거나 비트 단위로 0이 된 상태가 유효하지 않은 경우, 잘못된 배열 요소와 정의되지 않은 행동이 발생할 수 있습니다. 이러한 함수는 FMatrix나 FVector처럼 변경될 가능성이 거의 없는 타입의 배열에 가장 유용합니다.
기타
BulkSerialize 함수는 요소별로 직렬화하지 않고, 배열을 원시 바이트 블록으로 직렬화할 때 대체 operator<<로 사용할 수 있는 시리얼라이즈 함수입니다. 이렇게 하면 기본 제공 타입이나 일반 데이터 구조체와 같은 중요하지 않은 요소의 퍼포먼스를 개선할 수 있습니다.
CountBytes 및 GetAllocatedSize 함수는 배열에서 현재 사용 중인 메모리 양을 추정하는 데 사용됩니다. CountBytes는 FArchive를 받지만, GetAllocatedSize는 직접 호출될 수 있습니다. 이러한 함수는 일반적으로 통계 보고에 사용됩니다.
Swap 및 SwapMemory 함수는 모두 두 개의 인덱스를 받고 해당 인덱스에 있는 요소의 값을 교체합니다. 두 함수는 Swap이 인덱스에 대해 추가 오류 검사를 수행하고 인덱스가 범위를 벗어날 경우 어서트한다는 점을 제외하면 동일합니다.