3. TWeakObjectPtr (SmartPointer)

JUSTICE_DER·2023년 7월 20일
0

🌵UNREAL

목록 보기
34/42

🐸

[HUDWidget.h]

플레이어의 HP와 Exp를 띄우는 UI Widget이었다.
해당 코드 내에 TWeakObjectPtr을 사용했는데
도대체 저게 뭘까??


1. TWeakObjectPtr

1-1. CPP 11 스마트 포인터

TWeakObjectPtr을 알기 위해선
해당 포인터가 속한 집단을 알아야 한다.

우선 TWeakObjectPtr은 UE 스마트포인터 클래스에 속해있다.
해당 UE 스마트포인터 클래스는 CPP11 스마트포인터들의 커스텀 구현이라고 한다.

따라서 기존 CPP11의 스마트 포인터부터 알아야,
TWeakObjectPtr의 생성 이유를 알 수 있을 것이다.

아래의 글을 참고하였다

포인터 / 원시포인터 / 스마트포인터가 보인다.

결론부터 말하면, 일반적으로 사용했던 포인터가 원시포인터
(RawPointer)이다.

A. 원시포인터 (Raw Pointer)

#include <iostream>

int main() {
    int number = 42;
    
    // 1. int 형식을 가리키는 원시 포인터 선언
    int* ptr = &number;
    
    // 1-1. ptr을 이용하여 값에 접근
    std::cout << "Value of number: " << *ptr << std::endl; // Output: 42

    // 1-2. ptr을 이용하여 값 변경
    *ptr = 100;
    std::cout << "New value of number: " << *ptr << std::endl; // Output: 100
    
    // 2. 동적으로 메모리 할당
    int* dynamicPtr = new int;
    *dynamicPtr = 777;
    std::cout << "Dynamic pointer value: " << *dynamicPtr << std::endl; // Output: 777

    // 2-1. 동적으로 할당한 메모리 해제
    delete dynamicPtr;

    // 2-2. 동적으로 할당된 메모리 해제 후에도 접근 시 
    //undefined behavior (댕글링 포인터)
    std::cout << "After deletion, dynamic pointer value: " << *dynamicPtr << std::endl;
    
    return 0;
}

원시포인터는 그냥 주소를 담는 포인터이다.
( = 일반포인터 로 봐도 무방할 것이다)
1
포인터는 메모리의 스택영역에 저장되는 함수 내에 선언이 되었고,
같이 스택영역인 지역변수 number의 주소값을 받았다.
2
포인터는 메모리의 스택영역에 저장되는 함수 내에 선언이 되었고,
동적으로 할당된 힙메모리의 주소를 받았다.
해당 주소의 값은 수동으로 해제해주어야만 했고, delete를 사용하여 해제한다.

별 문제 없어 보이지만,
공식적으로 원시 포인터를 사용하지 않는 것을 권장한다.

그 이유는 2-2의 댕글링 포인터와 같은 이유 때문이다.
이미 delete한 메모리 공간을 포인터가 계속 가리키고 있는 오류이다.

Raw Pointer는 대표적으로 3가지 오류를 발생시킬 수 있다.

  • Dangling Pointer (허상)
    • 참조하는 Ptr이 존재하는데 메모리공간을 Delete해버림
  • 메모리누수
    • 더 이상 사용되지 않는 객체인데 delete를 하지 않음
  • 오류
    • 할당되지 않은 영역을 실수로 delete함

이 중에 메모리누수가 가장 빈번하게 발생하고,
Raw Pointer를 사용해서 발생하는 위의 문제를 해결하기 위해
CPP에선 C에서 없는 스마트포인터 개념을 도입하였다.

B. Smart Pointer

  • Smart Pointer는 기존의 Raw Pointer로부터
    메모리 누수가 없고, 예외로부터 안전한지 확인하도록 발전되었다.
void UseRawPointer()
{
    // Using a raw pointer -- not recommended.
    Song* pSong = new Song(L"Nothing on You", L"Bruno Mars"); 

    // Use pSong...

    // Don't forget to delete!
    delete pSong;   
}


void UseSmartPointer()
{
    // Declare a smart pointer on stack 
    // and pass it the raw pointer.
    unique_ptr<Song> song2(new Song(L"Nothing on You", L"Bruno Mars"));

    // Use song2...
    wstring s = song2->duration_;
    //...

} // song2 is deleted automatically here.

작성법은 = 을 사용하지 않고,
new를 괄호안에 넣어서 생성한다.
(원시포인터를 생성하고 스마트포인터로 형변환하는 느낌)

https://www.youtube.com/watch?v=DYSEulQoj8Q

CPP11 SmartPointer의 종류는 3가지 존재.

기본기능 : 포인터가 더이상 사용되지 않으면 자동으로 메모리 해제

  • unique_ptr<T>
    • 단순히 다른 포인터가 같은 객체를 가리킬 수 없도록 제한
    • 한 객체는 한 포인터만 가리킬 수 있게 된다
    • release()를 사용하면 소유권을 넘길 수 있다
    • reset()을 사용하여 가리키는 메모리의 값을 delete하고
      다른 객체를 가리키도록 할 수 있다.
  • shared_ptr<T>
    • 여러 포인터가 같은 객체를 가리킬 수 있도록 허용
    • use-count라는 요소가 존재
      • 해당 객체를 몇개의 포인터가 가리키고 있는지 감지
        (자동으로 소멸시킬 시점을 파악하기 위함)
      • use-count가 0이 되면 해당 객체도 삭제
      • use_count()를 사용하여 연결된 개수를 알 수 있다
  • weak_ptr<T>
    • 메모리에 대한 소유권은 없지만 가리키기만 한다는 의미
    • new를 통해 본인이 직접 메모리를 할당할 수 없다.
    • shared_ptr과 같이 쓰이기 위한 용도
      • shared_ptr과 같은 객체를 가리킬 경우에도
        weak_ptr은 use-count에 포함되지 않는다
      • 그리고 shared_ptr이 소멸되면,
        weak_ptr은 같이 소멸되지도 않는다
        (Dangling Pointer 오류 - 스마트포인터인데..)
      • 추가로 use_count()도 사용할 수 있다
        (물론 본인은 제외)
    • 다른 포인터와 다르게 바로 객체의 멤버함수, 변수 역참조가 불가능
      (위에서 본것처럼 Dangling Pointer오류가 있을 수 있으므로)
    • 따라서 expired()를 사용하여 가리키는 대상의 수명이
      끝났는지를 확인하고,
      lock()을 통해서 연결된 shared_ptr을 가져와서
      해당 변수로부터 호출을 해야한다.
      (그냥 lock()을 바로 사용해도 된다. 하지만 shared가 없다면 nullptr을 반환)

weak_ptr

  • 강한 참조 = 원래 쓰던 포인터 대입 방식
    • use count가 증가
    • RawPointer만 Dangling Pointer오류 발생가능
  • 약한 참조 = 명시적으로 weak_ptr을 사용하여 약하게 참조한다는 방식
    • use count는 증가하지 않음
    • Dangling Pointer오류 발생가능

1-2. Unreal Engine 스마트 포인터

아래의 글을 참고하였다

  • TUniquePtr
  • TWeakPtr
  • TSharedPtr
    • UObject를 가리킬 수 없다.
    • Native CPP 객체에 사용할 수 있다.

즉 위의 3가지 포인터는
기존 Native CPP를 위한 포인터라고 보면 되겠다.

언리얼은 리플렉션기능을 사용하면(= UPROPERTY 태그를 사용하면)
자동으로 GC시스템에 포함되어 메모리가 관리된다.
따라서 스마트 포인터가 필요 없다고 한다.

GC시스템은 shared_ptr과 유사하다
다른점은 GC의 특성상 포인터가 사용되지 않아도
메모리에서 해지되는 타이밍을 정확히 예측할 수 없다.

따라서 언리얼 오브젝트의 포인터를 소멸할 때에는 BeginConditionalDestroy()라는 함수를 호출해주고
해지해줄 때까지 그저 기다리는 수 밖에 없다고 한다.

shared_ptr에는 순환-참조 문제가 있었고,
그래서 생겨난 것이 weak_ptr이었다.

A. TWeakObjectPtr

마찬가지로 shared_ptr형식인 GC도 순환-참조 문제가 발생하므로
이를 해결하기 위해 등장한게 TWeakObjectPtr

특정 오브젝트를 참조해야하는데,
반드시 소유권이 필요하지 않은 경우

ex) Character는 Running의 기능을 소유하고 있는데
Running이 Character의 위치를 움직이는 작업을 위해서
Character의 멤버에 접근하기 위해 UPROPERTY를 사용한다면,
순환-참조 오류가 발생할 것이다.

따라서 위의 경우에 Running에서 Character를
TWeakObjectPtr로 선언하여 약참조를 걸어주는것이 바람직하다는 말이다.

결론
Native CPP는 (사용자가 만든 일반 클래스)
TUniquePtr/TWeakPtr/TSharedPtr를 사용하여 메모리를 관리한다.
UObject는 UPROPERTY() 혹은 TWeakObjectPtr
둘 중에 하나를 붙여야만 GC가 인식하고 메모리관리가 된다.

https://www.reddit.com/r/unrealengine/comments/osqia3/how_much_should_i_be_using_smart_pointers/

  • TWeakObjectPtr
    • UObject를 가리킬 수 있다.
  • TSharedRef
    • ?? 이건 또 무슨 개념이지?

즉 이득우 프로젝트에서 사용했던 코드를 다시보면,
해당 Widget은 StatComponent와 PlayerState에 대해
소유권을 가질 필요가 없어서 약참조를 한 것이다.

아마도 PlayerState에서 위젯을 소유한다면,
문제를 미리 막을 수 있기 때문에 사용하는 것으로 보인다.

하지만 한가지 의문이 남는다
CPP11에서 weak_ptr은 shared_ptr과 같이 사용되어야만 했는데,
어떻게 shared_ptr없이 weak_ptr인 TWeakObjectPtr이 사용되는 걸까??

shared_ptr의 역할을 GC가 대신하고 있는 것으로 생각하고 넘어가려고 한다..

profile
Time Waits for No One

1개의 댓글

comment-user-thumbnail
2023년 7월 20일

소중한 정보 감사드립니다!

답글 달기