안선생의 개발 블로그

[UE5] 언리얼 스마트 포인터 본문

언리얼

[UE5] 언리얼 스마트 포인터

안선생 2023. 12. 3. 20:56

언리얼엔진 메모리 관리

언리얼 엔진에서 메모리 관리는 중요한 부분이며, 스마트 포인터, 가비지 컬렉션, 객체 수명 주기 등을 포함한 여러 기술이 사용됩니다. 이러한 기술들은 메모리 누수를 방지하고 안정적인 게임 실행을 보장하기 위해 사용됩니다.

 

스마트 포인터

동적으로 할당한 메모리는 반드시 해제해주어야 하는데, 스마트 포인터는 이러한 동적 할당 메모리를 자동으로 해제해서 사고를 예방해준다. 프로그래머가 직접 Delete해줄 필요가 없습니다.

 

1. TUniquePtr

단일 소유권을 가지는 독점적인 포인터입니다. TUniquePtr는 특정 객체를 소유하고 해당 객체를 단일 포인터로서 관리하는데 사용됩니다.

 

예시입니다.

스마트 포인터는 Delete해줄 필요가 없습니다. 직접 해주기 때문

void ExampleFunction()
{
    // TUniquePtr을 사용하여 UMyObject 객체를 독점적으로 소유
    TUniquePtr<UMyObject> MyObject = MakeUnique<UMyObject>();

    // MyObject는 다른 스마트 포인터나 포인터로 이전할 수 없음
    // 단일 소유권을 가짐
}

 

  1. 단일 소유권:
    • TUniquePtr은 단일 소유권을 가지므로 한 객체에 대해 오직 하나의 TUniquePtr만이 소유할 수 있습니다.
  2. 이동 가능:
    • TUniquePtr은 이동 가능한 포인터로, 소유권을 이전하거나 이동시킬 수 있습니다. 이는 특히 함수 간 객체 소유권을 전달할 때 편리합니다.
  3. 메모리 관리:
    • TUniquePtr은 독점적으로 소유하는 특성상 별도의 참조 계수를 유지하지 않습니다. 객체가 TUniquePtr에서 해제되면 해당 객체의 메모리도 함께 해제됩니다.
  4. MakeUnique 함수:
    • MakeUnique 함수를 사용하여 동적으로 생성된 객체에 대한 TUniquePtr를 생성할 수 있습니다.

 

2. TSharedPtr

앞서 TUniquePtr에서 불가능했던 여러 포인터를가 같은 메모리를 가질 수 있습니다. 공유 참조 계수를 사용하여 객체의 수명을 관리하는 데에 쓰입니다. 이는 메모리 누수를 방지하고 효과적인 자원 관리를 도와줍니다.

void ExmpleFunction()
{
	{
		// 참조 계수 증가 == 1
		TSharedPtr<UMyObject> MyObject = MakeShareable(New UMyObject());
		{
			// 참조 계수 증가 == 2
			TSharedPtr<UMyObject> MyObject1 = Data;
            
			// 참조 계수 증가 == 3
			TSharedPtr<UMyObject> MyObject2 = Data;
		}
		// MyObject1 파괴 -> 참조 계수 감소 == 2
		// MyObject2 파괴 -> 참조 계수 감소 == 1
	}	
	// MyObject 파괴 -> 참조 계수 감소 == 0 -> 메모리 해제
}

 

TSharedPtr는 메모리 관리와 참조 관리에 있어서 언리얼 엔진에서 주로 사용되는 기술 중 하나입니다. 개발자들은 객체 수명 주기와 참조 계수 관리를 쉽게 처리할 수 있도록 도와줍니다.

 

하지만 이 포인터에는 순환참조라는 큰 문제점있습니다.

 

class UMyObject
{
	TSharedPtr<UMyObject> OtherObject;
};


void ExmpleFunction()
{
	{
		// A 참조 계수 증가 == 1
		TSharedPtr<UMyObject> A = MakeShareable(new UMyObject());

		// B 참조 계수 증가 == 1
		TSharedPtr<UMyObject> B = MakeShareable(new UMyObject());

		// B 참조 계수 증가 == 2
		A->OtherObject = B;

		// A 참조 계수 증가 == 2
		B->OtherObject = A;
	}
	// 이 시점에 두 객체의 메모리가 모두 해제되길 기대하지만,
	// A 파괴 -> A 참조 계수 감소 == 1
	// B 파괴 -> B 참조 계수 감소 == 1
	// TSharedPtr만 파괴되고, 할당된 객체 메모리들은 서로 참조하며 죽지 않고 남아있게 됨.
}

순환 참조는 객체들 간에 서로를 참조하는 상황을 의미합니다. 이 경우, 강한 참조로 인해 가비지 컬렉션에서 메모리 해제가 올바르게 이루어지지 않아 메모리 누수가 발생할 수 있습니다. 언리얼 엔진에서는 이러한 문제를 방지하기 위해 약한 참조(TWeakPtr)를 사용할 수 있습니다.

 

3. TWeakPtr

TWeakPtr는 언리얼 엔진에서 사용되는 스마트 포인터로, 약한 참조(Weak Reference)를 나타냅니다. 주로 순환 참조를 방지하고 메모리 누수를 예방하기 위해 사용됩니다.

약한 참조는 다음과 같은 특징을 가지고 있습니다:

  1. 참조 계수 증가 없음:
    • TWeakPtr는 객체의 참조 계수를 증가시키지 않습니다. 따라서 객체를 약한 참조하는 동안에는 참조 계수에 영향을 주지 않습니다.
  2. 객체의 소멸 여부 확인:
    • TWeakPtr를 사용하여 약한 참조하는 경우, 해당 객체가 이미 소멸되었는지 여부를 확인할 수 있습니다.
  3. 유효성 검사:
    • TWeakPtr를 사용하여 약한 참조할 때는 항상 유효성을 검사하여 해당 객체가 유효한지 확인하는 것이 좋습니다.

 

다음은 약한 참조를 사용하여 순환 참조 문제를 해결하는 예제입니다.

 

class UMyObject : public UObject
{
public:
    TWeakPtr<UMyObject> WeakReference;

    UMyObject()
    {
        // 약한 참조 초기화
        WeakReference = SharedThis(this);
    }
};

int main()
{
    // 객체 생성 카운트 1씩증가
    TSharedPtr<UMyObject> ObjectA = MakeShared<UMyObject>();
    TSharedPtr<UMyObject> ObjectB = MakeShared<UMyObject>();

    // 약한 참조를 사용하여 순환 참조 문제 회피
    // 카운트가 증가하지 않음
    ObjectA->WeakReference = ObjectB;
    ObjectB->WeakReference = ObjectA;

    // 유효성 검사를 통해 객체가 아직 존재하는지 확인
    if (TSharedPtr<UMyObject> LockedObject = ObjectA->WeakReference.Pin())
    {
        // 유효한 경우에 수행할 작업
        LockedObject->Fcuntion();
    }

    return 0;
}
//카운트 감소 파괴

이 예제에서 WeakReferenceTWeakPtr를 사용하여 약한 참조를 하고 있습니다. Pin() 함수를 사용하여 약한 참조한 객체가 여전히 유효한지 확인하고, 유효한 경우에 공유 포인터로 변환 후 해당 객체에 접근할 수 있습니다.

 

 

'언리얼' 카테고리의 다른 글

[UE5] 리플렉션  (0) 2023.12.14
[UE5] 언리얼 가비지 컬렉션  (0) 2023.12.09
[UE5] TWeakObjetPtr  (0) 2023.11.27
[UE5] 애니메이션 블루프린트  (0) 2023.11.13
[UE5] Cast  (0) 2023.11.11