< [이득우의 언리얼 프로그래밍 Part1 필기] 10 - 11. TArray, TSet, 구조체, TMap

언리얼엔진5/[Part1] 이득우의 언리얼 프로그래밍

[이득우의 언리얼 프로그래밍 Part1 필기] 10 - 11. TArray, TSet, 구조체, TMap

Rocketbabydolls 2024. 2. 20. 20:43

C++ STL과 언리얼 컨테이너 라이브러리의 차이점

C++ STL 언리얼 컨테이너 라이브러리 (언리얼 자체제작)
범용적으로 설계되어 있다. 언리얼 엔진에 특화되어 있다.
표준이기 때문에 호환성이 높다. 언리얼 오브젝트 구조를 안정적으로 지원한다.
많은 기능이 엮여 있어 컴파일 시간이 오래 걸림. 가볍고 게임 제작에 최적화되어 있음.

 

TArray - 오브젝트를 순서대로 담아 효율적으로 관리하는 용도

TSet - 중복되지 않는 요소로 구성된 집합을 만드는 용도로 사용

TMap - 키, 밸류 조합의 레코드를 관리하는 용도로 사용

 

TArray

TArray 의 구조

 

  • C++ STL 의 Vector 와 유사하다.
  • 동적할당 불가 / 가변 배열
  • 데이터가 순차적으로 모여있어서 효과적 사용이 가능하다.
  • 캐시 효율이 높다. 
  • 배열의 맨 끝 추가는 가볍지만 중간에 요소 추가 및 삭제는 비용이 크다.

 

자주 쓰이는 함수

Add 밖에서 생성 후 복사해서 삽입
Emplace 내부에서 바로 생성 (복사 X)
SetNum 배열 크기 조정
Find 순회하며 요소 찾음. 효율이 좋지 않을 수 있다.
MoveTemp 배열 데이터 통째로 이주
== 요소 자료형과 배열의 크기가 같을 때의 비교 연산자.
FString 시 대소문자 구분 X 
Sort 정렬

 

나머지 함수들은 공식 문서에 잘 정리되어 있다. 활용성이 높아 보이므로 자주 읽어 보는 것이 좋아보인다.

 

공식 문서

 

TArray: 언리얼 엔진의 배열

 

docs.unrealengine.com

const int32 ArrayNum = 10;
	TArray<int32> Int32Array; # TArray 선언 

	for (int32 ix = 1; ix <= ArrayNum; ++ix)
	{
		Int32Array.Add(ix);
	}

	Int32Array.RemoveAll(
		[](int32 Val)
		{
			return Val % 2 == 0;  # RemoveAll 함수에서 Val 전달
		}
	);

	Int32Array += {2, 4, 6, 8, 10};

	TArray<int32> Int32ArrayCompare;
	int32 CArray[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8 , 10 };
	Int32ArrayCompare.AddUninitialized(ArrayNum); # 배열 크기 설정
	FMemory::Memcpy(Int32ArrayCompare.GetData(), CArray, sizeof(int32) * ArrayNum);  # Memcpy 를 통한 배열 복사

	ensure(Int32Array == Int32ArrayCompare);

	int32 Sum = 0;
	for (const int32& Int32Elem : Int32Array)  # 읽기 전용 순회
	{
		Sum += Int32Elem;
	}

	ensure(Sum == 55);

	int32 SumByAlgo = Algo::Accumulate(Int32Array, 0);  # sum 작용을 하는 accumulate함수가 #include "Algo/Accumulate.h" 에 포함되어 있다.
	ensure(Sum == SumByAlgo);

 

슬랙

  • 배열은 크기변경이 가능하므로, 메모리 사용량이 가변적입니다. 배열이 추가될 때마다 매번 재할당을 피하기 위해, 얼로케이터는 보통 요청한 것보다 넉넉한 메모리를 제공하여 앞으로의 Add 호출시 재할당에 드는 퍼포먼스 비용을 물지 않도록 합니다. 마찬가지로 엘리먼트를 삭제한다고 메모리가 해제되지는 않으며, 배열에 slack (여유분, 슬랙), 즉 현재 사용되지는 않아도 사실상 미리 할당된 엘리먼트 저장 슬롯을 남길 뿐입니다. 배열의 슬랙 양은 현재 컨테이너에 있는 엘리먼트의 수와, 엘리먼트를 몇 개나 더 추가하면 다음 할당이 일어나는지에 대한 차이로 정의할 수 있습니다.

요약하자면 배열을 선언하고 엘리먼트를 추가하면 넉넉하게 메모리를 잡고 있는다는 말이다. 엘리먼트 삭제 시에 메모리가 바로 해제되지는 않는다. Shrink 함수로 슬랙 공간 제거 가능. 슬랙 공간을 이용하면 최적화에 대한 힌트를 얻을 수 있다. ex) 배열에 100개의 요소를 더 추가해야 하는 상황에, 슬랙이 100 이상 된다는 것을 경험적으로 알고 있다면, 배열의 크기를 늘리는 작업을 제외하고 그냥 추가할 수 있다.

 

 

TSet

C++ STL Set 언리얼 TSet
이진 트리로 구성되어 있어 정렬을 지원한다.  해시테이블 형태로 키 데이터가 구축되어 있어 빠른 검색이 가능하다.
O(1)
메모리 구성이 효율적이지 않다. 동적 배열의 형태로 데이터가 모여있음.
요소가 삭제될 때 균형을 위한 재구축이 일어날 수 있음. 데이터를 삭제해도 재구축이 일어나지 않음
모든 자료를 순회하는 데 적합하지 않다. 비어있는 데이터가 있을 수 있다. (Invalid)

TSet의 구조

  • RemoveAll 함수가 없다.
  • 데이터를 삭제 하고 삽입 시 마지막 Invalid 화 된 공간에 add 된다.
  • Compact 를 사용해 Invalid를 없앨 수 있다.

 

공식 문서 (더 많은 메서드는 아래 문서 참고)

 

TSet

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

docs.unrealengine.com

시간 복잡도 비교

 

UStruct

  • 데이터 저장 / 전송에 특화된 가벼운 객체
  • 기본적으로 전역 선언이다.
  • 대부분 GENERATED_BODY 매크로를 선언해준다.
    -> 리플렉션, 직렬화와 같은 유용한 기능 지원
    -> GENERTATED_BODY 를 선언한 구조체는 UScriptStruct 클래스로 구현됨
    -> 제한적으로 리플렉션 지원 ( 속성 UPROOPERTY 만 선언할 수 있고 UFUNCTION 은 선언 불가
  • 언리얼 엔진의 구조체 이름은 F로 시작함
    -> 대부분 힙 메모리 할당(포인터 연산) 없이 스택 내 데이터로 사용됨.
    -> NewObject API 를 사용할 수 없음.

 

 

TMap

  • STL map 의 특징
    이진 트리로 구성되어 있음
    정렬은 지원하지만, 메모리 구성이 효율적이지 않으며, 데이터 삭제 시 재구축이 일어날 수도 있다.
    모든 자료를 순회하는데 적합하진 않음
  • 언리얼 TMap의 특징
    키, 밸류 구성의 튜플 데이터의 TSet 구조로 구현되어 있음
    해시테이블 형태로 구축되어 있어 빠른 검색이 가능하다.
    동적 배열의 형태로 데이터가 모여 있다.
    데이터를 삭제해도 재구축이 일어나지 않는다.
    비어있는 데이터가 있을 수 있다.
    TMultiMap 을 사용하면 중복 데이터를 관리할 수 있음.

모식도

 

++ 추가)

값 비교 시 Key값으로 비교한다.

Find를 통해 한번에 인덱스를 가져올 수 있다.

동질성 컨테이너이다.

TPair를 채택했다.

 

선언 방법

TMap<int32, FString> StudentsMap;

 

공식 문서

 

TMap

TMap, 맵은 크게 키 유형과 값 유형, 두 가지로 정의되며, 맵에 하나의 짝으로 저장됩니다.

docs.unrealengine.com

컨테이너들의 시간 복잡도

 

추가사항 

 

algo::transform 을 사용하면 컨테이너끼리 변환이 가능하다. 

 

# TArray -> TArray

TArray<FString> AllStudentsNames;
	Algo::Transform(StudentsData, AllStudentsNames,
		[](const FStudentData& Val)
		{
			return Val.Name;
		}
	);

# TArray -> TSet

	TSet<FString> AllUniqueNames;
	Algo::Transform(StudentsData, AllUniqueNames,
		[](const FStudentData& Val)
		{
			return Val.Name;
		}
	);

# TArray -> TMap

	Algo::Transform(StudentsData, StudentsMap,
		[](const FStudentData& Val)
		{
			return TPair<int32, FString>(Val.Order, Val.Name);
		}
	);

# TArray -> TMultiMap

	TMultiMap<FString, int32> StudentMapByName;
	Algo::Transform(StudentsData, StudentMapByName,
		[](const FStudentData& Val)
		{
			return TPair<FString, int32>(Val.Name, Val.Order);
		}
	);

 

 

커스텀 구조체에 대한 gettypehash 함수가 지정 안되어있다면 에러가 뜬다. 아래 두 가지 사항을 구현해 줄 것.

  1. operator ==
  2. gettypehash 함수 
USTRUCT()
struct FStudentData
{
	GENERATED_BODY()
	
	FStudentData()
	{
		Name = TEXT("홍길동");
		Order = -1;
	}

	FStudentData(FString InName, int32 InOrder) : Name(InName), Order(InOrder) {}

	bool operator==(const FStudentData& InOther) const
	{
		return Order == InOther.Order;
	}

	friend FORCEINLINE uint32 GetTypeHash(const FStudentData& InStudentData)
	{
		return GetTypeHash(InStudentData.Order);
	}

	UPROPERTY()
	FString Name;

	UPROPERTY()
	int32 Order;
};

 

 

GetTypeHash 함수에 friend 키워드를 붙인 이유

friend를 선언하면 구조체의 멤버 함수였던 gettypehash 함수가 구조체 변수에 접근이 가능하면서 전역으로 사용이 가능한 전역 함수가 된다.

 

A friend function is a function that isn't a member of a class but has access to the class's private and protected members. Friend functions aren't considered class members; they're normal external functions that are given special access privileges. Friends aren't in the class's scope, and they aren't called using the member-selection operators (. and ->) unless they're members of another class. A friend function is declared by the class that is granting access. The friend declaration can be placed anywhere in the class declaration. It isn't affected by the access control keywords.

 

https://learn.microsoft.com/en-us/cpp/cpp/friend-cpp?view=msvc-170

 

friend (C++)

Learn more about: friend (C++)

learn.microsoft.com