언리얼 - 자료구조 (TArray)

안영욱·2023년 2월 12일
0

언리얼

목록 보기
1/5

언리얼의 자료구조 TArray 에대해 알아보겠습니다.


TArray는 언리얼엔진4 에서 가장 간단한 배열 컨테이너 클래스
TArray유형이 같은 다른오브젝트 들을 순서대로 정리하여 저장하는 클래스이다.
TArray는 시퀀스 형태로 함수를 이용하여 오브젝트와 순서를 결정지을 수 있다.


1. TArray

TArray는 언리얼에서 가장 자주쓰이는 컨테이너 클래스로 신속성, 메모리 효율성, 안정성을 염두하여 디자인 되었다.
TArray는 주로 2가지 프로퍼티로 구분되며 *엘리먼트(element) 유형 *할당자(allocator) 입니다.

엘리먼트(element)

  • 엘리먼트는 배열에 저장되는 오브젝트 유형
  • TArray는 동질성 컨테이너로 저장되는 엘리먼트의 유형이 엄격히 같아야하며 유형이 다른 엘리먼트는 저장할수없다.

할당자(allocator)

  • 할당자 는 메모리에 오브젝트가 레이아웃되는 방식, 배열에 엘리먼트를 넣어 배열을 키우는방식을 결정
  • 대부분 생략되는 경우가많으며 (기본값은 1) 대부분 적합한 값으로 사용된다.
  • 할당자를 다루는 방식은 여러가지가 있으며 기본 작동적합하지 않을경우 직접 작성하여 사용한다.

TArray는 값 유형은 int32 ,float와 같은 내장형 자료형과 비슷하게 취급해야한다.
TArray는 확장을 염두해두지 않았기때문에 인스턴스를 newdelete생성, 삭제 를하는것은 좋지않다.
TArray의 엘리먼트는 값유형이기도하며 배열이 소유한다.
TArray의 소멸은 소속 엘리먼트의 소멸로 이어진다.
TArray변수를 다른 TArray엘리먼트로 할당해도 엘리먼트를 복사하여 할당하며 엘리먼트가 공유되지는 않는다.


2. TArray를 생성하고 할당하기

2-1. 생성자

TArray에 정수를 담아 사용하기위해 빈배열을 생성.

TArray<int32> IntArray; //자료형<엘리먼트의자료형> 변수이름
  • 엘리먼트의 자료형으로는 int32, FString, TSharedPtr 과같은 C++에서 복사 및 소멸이 가능한 유형이라면 어떤것이든 가능하다.

2-2. Init

TArray를 채우는 방식은 여러가지가 존재하며 그중 Init함수는 배열을 엘리먼트의 복사본 여러개로 채우는것이다.

IntArray.Init(10, 5); // IntArray == [10,10,10,10,10] // 10복사본 5개를 할당

2-3. Add , Emplace

AddEmplace 함수를 사용해서 배열 끝에 새 오브젝트를 만들 수 있다.

TArray<FString> StrArr;
StrArr.Add    (TEXT("Hello")); //Add를 이용하여 Text 타입의 "Hello" 문자열을 배열끝에 할당
StrArr.Emplace(TEXT("World")); //Emplce를 이용하여 Test타입의 "World" 문자열을 배열끝에 할당
// StrArr == ["Hello","World"]

TArray얼로케이터는 엘리먼트가 추가될때마다 필요에따라 메모리를 제공한다.
기본 얼로케이터는 다수의 엘리먼트가 추가될때마다 충분한 양의 메모리를 제공한다.

AddEmplace의 동작방식은 미묘한 차이가 있다.

  • Add(또는 Push) 는 엘리먼트 유형의 인스턴스를 배열에 복사 (또는 이동)한다.
  • Emplace 는 지정한 인수를 사용하여 엘리먼트 유형의 인스턴스를 새로 생성한다.

TArray<FString>의 경우

  • 'Add'는 스트링 리터널을 사용해 임시FString리터널을 생성후 컨테이너안의 새로운FString으로 이동시킨다.
  • 'Emplace'는 스트링 리터널을 사용해 FString 을 직접 만든다.

최종 결과는 같지만, Emplace임시 변수 생성을 하지 않는다.
FString 처럼 복잡한 값 유형은 퍼포먼스상 바람직하지 않은 경우가 많기 때문이다.

  • 일반적으로 Emplace Add보다 좋은점은 불필요한 복사, 이동이 없다는점!
  • 사소한 경우 Add그외에는 Emplace를 권장한다.

2-4. Append

Append 는 다른 TArray 또는 일반 C배열로의 포인터 및 해당 배열의 크기에 다수의 엘리먼트를 한꺼번에 추가한다.

FString Arr[] = { TEXT("of"), TEXT("Tomorrow") };
StrArr.Append(Arr, ARRAY_COUNT(Arr));
// StrArr == ["Hello","World","of","Tomorrow"]

2-5. AddUnique

AddUnique 는 기존배열에 동일한 엘리먼트가 존재하지 않는 경우 새 엘리먼트만 추가한다.

  • 존재 여부는 엘리먼트 유형의 operator== 를 사용해서 검사한다.
StrArr.AddUnique(TEXT("!"));
// StrArr == ["Hello","World","of","Tomorrow","!"]
StrArr.AddUnique(TEXT("!"));
// StrArr is unchanged as "!" is already an element

2-6. Insert

InsertAdd, Emplace, Append 처럼 단일 엘리먼트나 엘리먼트 배열 사본을 지정한 위치에 추가한다.

StrArr.Insert(TEXT("Brave"), 1);
// StrArr == ["Hello","Brave","World","of","Tomorrow","!"]

2-7. SetNum

SetNum함수는 배열의 크기를 직접 설정할 수 있다.
설정된 크기가 현재 배열 크기보다 큰 경우 기본 생성자의 엘리먼트 유형을 사용해서 엘리먼트를 새로 만든다.

StrArr.SetNum(8);
// StrArr == ["Hello","Brave","World","of","Tomorrow","!","",""]

SetNum 의 설정크기가 현재 배열 크기보다 작은 경우 엘리먼트를 제거하기도 한다.

StrArr.SetNum(6);
// StrArr == ["Hello","Brave","World","of","Tomorrow","!"]

3. 반복처리

TArray 의 엘리먼트에 대한 반복처리(iterate)를 하는 방법은 여러가지 있으나, C++ 의 범위 for 기능을 사용하는 것을 권장한다.

FString JoinedStr;
for (auto& Str : StrArr)
{
    JoinedStr += Str;
    JoinedStr += TEXT(" ");
}
// JoinedStr == "Hello Brave World of Tomorrow ! "

3-1. 인덱스 기반의 반복처리

for (int32 Index = 0; Index != StrArr.Num(); ++Index)
{
    JoinedStr += StrArr[Index];
    JoinedStr += TEXT(" ");
}

3-2. CreateIterator, CreateConstIterator

배열 반복처리에 대한 보다 세밀한 제어가 가능하다.

  • CreateIterator - 읽기,쓰기
  • CreateConstIterator - 읽기전용
 for (auto It = StrArr.CreateConstIterator(); It; ++It)
{
    JoinedStr += *It;
    JoinedStr += TEXT(" ");
}

4. 배열정렬(Sorting)

4-1. Sort

배열은 Sort 함수를 호출하는 것으로 간단히 정렬 가능하다.

StrArr.Sort();
// StrArr == ["!","Brave","Hello","of","Tomorrow","World"] //알파벳 순서로 정렬됨
  • 여기서 엘리먼트 유형 연산자 < 를 사용해서 값을 정렬한다.
  • 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간단한 정렬을 위해 구현된 기능

4-2. HeapSort

HeapSort은 이진 술부(predicate)가 없어도 힙(Heap) 소팅(Sort) 에 사용 가능하다.

  • 데이터의 종류와 Sort 함수에 비할 때 소팅의 효율성에 따라 선택됩니다.
  • Sort 처럼 HeapSort안정적이지 못합니다.

HeapSort로 정렬한 결과 ▽

StrArr.HeapSort([](const FString& A, const FString& B) {
    return A.Len() < B.Len();
});
// StrArr == ["!","of","Hello","Brave","World","Tomorrow"]

4-3. StableSort

StavleSort정렬 이후 동등한 엘리먼트의 순서를 결정하는데 사용된다.

위에서 Sort 나 HeapSort 대신 StableSort 를 사용했다면 ▽

StrArr.StableSort([](const FString& A, const FString& B) {
    return A.Len() < B.Len();
});
// StrArr == ["!","of","Brave","Hello","World","Tomorrow"]

"Brave", "Hello", "World" 가 기존 사전식으로 정렬됩니다. StableSort 는 병합 소트로 구현되었다.


5. 쿼리(Query)

5-1. Num

Num 함수를 사용해서 배열에 엘리먼트가 몇 개인지 확인할 수 있다.

int32 Count = StrArr.Num();
// Count == 6

5-2. Getdata

C 스타일 API 같은 것과의 상호 정보 교환을 위해 배열 메모리에 직접 접근할 필요가 있는 경우 사용됨

  • GetData함수를 사용해서 배열 내 엘리먼트에 대한 포인터를 반환시킬 수 있다.
  • 이 포인터는 배열이 존재하는 한에서, 그리고 배열이 변형되기 전에만 유효하다.
  • 오직 StrPtr 에서의 Num 인덱스만이 역참조가 가능(dereferenceable)하다.
FString* StrPtr = StrArr.GetData();
// StrPtr[0] == "!"
// StrPtr[1] == "of"
// ...
// StrPtr[5] == "Tomorrow"
// StrPtr[6] - undefined behavior

컨테이너가 const 인 경우, 반환되는 포인터 역시 const 이다.


5-3. GetTypeSize

컨테이너의 엘리먼트 타입의 크기도 알아낼수 있다.

uint32 ElementSize = StrArr.GetTypeSize();
// ElementSize == sizeof(FString)

5-4. Operator[]

엘리먼트 값을 얻으려면, operator[] 인덱싱을 사용해 원하는 엘리먼트에 대한 인덱스 값을 입력한다.

FString Elem1 = StrArr[1];
// Elem1 == "of"

operator[] 는 레퍼런스를 반환하므로, 배열이 const가 아니라는 가정하에 배열 내의 엘리먼트를 수정할 수도있다.

StrArr[3] = StrArr[3].ToUpper();
// StrArr == ["!","of","Brave","HELLO","World","Tomorrow"] // hello를 대문자로 변환

GetData 함수처럼 operator[] 도 배열이 const 인 경우 const 레퍼런스를 반환한다.


5-5. IsValidIndex

유효하지 않은 인덱스, 즉 0 미만이거나 Num() 이상 값을 전해주면, 실행시간 오류가 발생한다.
컨테이너에 특정 인덱스가 유효한지 IsValidIndex 함수를 통해 확인할수 있다.

bool bValidM1 = StrArr.IsValidIndex(-1); // 0미만
bool bValid0  = StrArr.IsValidIndex(0);
bool bValid5  = StrArr.IsValidIndex(5);
bool bValid6  = StrArr.IsValidIndex(6); // 최대값 Num을넘김
// bValidM1 == false
// bValid0  == true
// bValid5  == true
// bValid6  == false

5-6. Last, Top

Last 함수를 사용하여 배열 끝에서부터 역순으로 인덱스를 사용할 수도 있다.
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"

5-7. Contains, ContainsByPredicate

배열에 특정 엘리먼트가 있는지 확인하는데 사용됨

bool bHello   = StrArr.Contains(TEXT("Hello"));
bool bGoodbye = StrArr.Contains(TEXT("Goodbye"));
// bHello   == true
// bGoodbye == false

ContainsByPredicate 는 배열에 지정된 술부(predicate)와 일치하는 엘리먼트가 있는지 확인할수 있다.

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 == false

5-8. Find, FindLast

Find 함수를 사용하여 엘리먼트를 찾을 수 있다.

  • 반환값은 해당 엘리먼트가 있는 인덱스 값을 반환한다.
int32 Index;
if (StrArr.Find(TEXT("Hello"), Index))
{
    // Index == 3
}

Find함수는 엘리먼트가 중복될경우 첫번째 엘리먼트를 반환한다.
FindLast함수를 사용하면 마지막 엘리먼트를 반환한다.

int32 IndexLast;
if (StrArr.FindLast(TEXT("Hello"), IndexLast))
{
    // IndexLast == 3, because there aren't any duplicates
}
  • Find, FindLast 모두 반환값이 bool로 반환된다.

FindFindLast 는 엘리먼트 인덱스를 직접 반환할 수도 있다.

엘리먼트를 찾지 못했으면, 특수 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_NONE

5-9. IndexOfByKey ,IndexOfByPredicate

IndexOfByKey함수는 엘리먼트와 다른 타입과 비교가 가능하다.

  • Find의 경우 인수를 엘리먼트 유형으로 변환 하는과정을 거친다.
  • IndexOfByKey의 경우 엘리먼트와 key바로 비교한다.
  • key를 엘리먼트 형태로 변환할수 없을때에도 사용가능하다.

IndexOfByKeyoperator==(ElementType, KeyType) 가 존재할경우 작동한다.
IndexOfByKey처음 찾은 엘리먼트의 인덱스를 반환한다.
찾은 것이 없으면 INDEX_NONE 을 반환한다.

int32 Index = StrArr.IndexOfByKey(TEXT("Hello"));
// Index == 3

IndexOfByPredicate함수는 설정한 술부로 일치하는 첫 엘리먼트를 반환한다.
찾은 것이 없으면 마찬가지로 특수 INDEX_NONE 값을 반환한다.

int32 Index = StrArr.IndexOfByPredicate([](const FString& Str){
    return Str.Contains(TEXT("r"));
});
// Index == 2

5-10. FindByKey, FindByPredicate

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

5-11. FilterByPredicate

FilterByPredicate 함수는 설정한 술부의 조건과 일치하는 엘리먼트 배열을 반환한다.

auto Filter = StrArray.FilterByPredicate([](const FString& Str){
    return !Str.IsEmpty() && Str[0] < TEXT('M');
});

6. 제거

6-1. Remove

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]

6-2. RemoveSingle

RemoveSingle로 배열에서 처음 찾아내는 엘리먼트를 지울수 있다.

  • 배열에 중복된 것이 있는데 하나만 지우고자 한다거나, 배열에 해당 엘리먼트가 딱 하나만 있는 것이 확실한 경우 최적화 차원에서 유용하게 사용할수 있다.
ValArr.RemoveSingle(30);
// ValArr == [10,5,10,15,25,30]

6-3. RemoveAt

RemoveAt함수로 제거할 엘리먼트를 인덱스로 지정할 수 있다.

  • IsValidIndex 로 인덱스를 한번 검증하고 사용하는것이 좋은데, 배열에 없는 인덱스를 사용할경우 런타임 오류가 발생한다.
ValArr.RemoveAt(2); // 인덱스 2 엘리먼트를 제거합니다
// ValArr == [10,5,15,25,30]
ValArr.RemoveAt(99); // 런타임 오류가 발생합니다
// 인덱스 99 에 엘리먼트가 없기 때문입니다

6-4. RemoveAll

RemoveAll함수로 설정한 술부와 일치하는 모든 엘리먼트를 제거한다.
예) 3의 배수인 값을 전부 제거

ValArr.RemoveAll([](int32 Val) {
    return Val % 3 == 0;
});
// ValArr == [10,5,25]
  • 엘리먼트가 제거되면 그뒤 엘리먼트가 당겨져 배열에는 구멍이 생길수 없다.

6-5. Swap

제거 프로세스에는 비용이 따른다..

  • RemoveSwap, RemoveAtSwap, RemoveAllSwap 함수를 사용해서 부하를 줄일 수 있다.
  • Swap을 이용하면 부하가 줄어들지만, 배열의 순서는 보장받을수 없다.
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]
ValArr2.RemoveAllSwap([](int32 Val) {
    return Val % 3 == 0;
});
// ValArr2 == [1,4,4]

6-6. Empty

Empty 함수는 배열에서 모든 것을 제거한다.

ValArr2.Empty();
// ValArr2 == []

7. 연산자(operator)

배열은 일반적인 생성자 복사할당 연산자를 통해 복사할 수 있다.
배열은 엘리먼트를 엄격히 소유하기에, 새 배열에는 자체적인 엘리먼트 사본이 생긴다..

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];

7-1. +=

Append 함수의 대안으로 , operatoe+=를 통해 배열을 추가할수 있다.

ValArr4 += ValArr3;
// ValArr4 == [5,2,3,1,2,3]

7-2. MoveTemp

MoveTemp 함수를 이용하여 배열을 이동시킬수 있다. 이동 이후 원본 배열은 공백으로 남는다.

ValArr3 = MoveTemp(ValArr4);
// ValArr3 == [5,2,3,1,2,3]
// ValArr4 == []

7-3. ==, !=

배열은 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
for (auto& Str : FlavorArr2)
{
    Str = Str.ToUpper();
}
// FlavorArr2 == ["CHOCOLATE","VANILLA"]
bool bComparison2 = FlavorArr1 == FlavorArr2;
// bComparison2 == true, because FString comparison ignores case
Exchange(FlavorArr2[0], FlavorArr2[1]);
// FlavorArr2 == ["VANILLA","CHOCOLATE"]
bool bComparison3 = FlavorArr1 == FlavorArr2;
// bComparison3 == false, because the order has changed

출처
언리얼 공식 홈페이지


언리얼 TArray가 가지고있는 여러 함수들을 알아보았습니다
바로바로 이해가가지않는 부분은 제나름데로 수정을하면서 공부해보았습니다.
수정할부분이 있다면 댓글로 알려주시면 감사합니다.

profile
개발자좀 한번해보자

0개의 댓글