[언리얼 엔진] 스마트 포인터

    언리얼 스마트 포인터 라이브러리

    언리얼 스마트 포인터 라이브러리(Unreal Smart Point Library)는 메모리 할당과 추적의 부담을 해소하도록 설계된 C++11 스마트 포인터들의 커스텀 구현이다. 표준인 Shared Pointer, Weak Pointer, Unique Pointer 와 null이 불가능한 Shared Reference 가 포함되어있다. 언리얼 오브젝트는 게임 코드에 더 최적화된 별도의 메모리 추적 시스템을 사용하기 때문에 Uobject 시스템과 사용할 수 없다.

    스마트 포인터타입

    Shared Pointer(TSharedPtr)

    쉐어드 포인터는 참조하는 오브젝트를 소유하며, 무기한으로 오브젝트의 소멸을 방지하고, 참조하는 쉐어드 포인터나 쉐어드 레퍼런스가 없을 경우에 오브젝트를 소멸시킨다. 쉐어드 포인터는 어느 오브젝트도 참조하지 않는 빈 상태일 수 있다.

    Shared Reference(TSharedRef)

    쉐어드 레퍼런스는 참조하는 오브젝트를 소유하는 측면에서 쉐어드 포인터와 같은 역할을 한다. 쉐어드 레퍼런스는 항상 null이 불가능한 오브젝트를 참조해야 한다.

    Weak Pointer(TWeakPtr)

    위크 포인터는 쉐어드 포인터와 비슷하지만 참조하는 오브젝트를 소유하지 않기 때문에 생명 주기에 영향을 주지 않는다. 이러한 속성은 참조 주기에 영향을 주지 않기 때문에 유용하지만 언제든지 null이 될수 있다는 뜻이다.

    Unique Pointer(TUniquePtr)

    유니크 포인터는 참조하는 오브젝트를 유일하고 명시적으로 소유한다. 특정 자원에 대해서 하나의 유니크 포인터만 존재하기 때문에, 유니크 포인터는 소유권을 이전할 수 있지만 공유할 수는 없다. 유니크 포인터를 복사하려면 컴파일 오류가 발생한다. 유니크 포인터가 스코프를 벗어나게 되면 참조하는 오브젝트는 자동 소멸된다.

    스마트 포인터의 이점

    • 메모리 누수 방지 - 위크를 제외한 스마트 포인터들을 공유된 레퍼런스가 없으면 오브젝트가 자동 소멸된다.
    • 위크 레퍼런싱 - 위크 포인터는 참조 주기에 영향을 주지 않고, 삭제된 오브젝트를 참조(Dangling) 포인터를 방지한다.
    • 선택적인 스레드 안전 - 언리얼 스마트 포인터 라이브러리에 멀티스레드에 걸쳐 참조 카운팅을 관리하는 코드인 스레드 세이프 코드가 포함되어 있다.
    • 런타임 안정성 - 쉐어드 레퍼런스는 절대 null일수 없으며 언제든지 참조 해제될 수 있다.
    • 명확한 의도 - 관찰자 중에서 오브젝트의 소유자를 쉽게 분별할 수 있다.
    • 메모리 - 스마트 포인터는 64비트의 C++ 포인터 크기의 두배이다. 유니크 포인터만 C++ 포인터의 크기와 같다.

    헬퍼 클래스와 함수

    클래스

    TSharedFromThis - TSharedFromThis에서 클래스를 파생 시키면 AsShared 혹은 SharedThis 함수가 추가된다. 이러한 함수들을 통해 TSharedRef를 구할 수 있다.

    함수

    MakeShared 및 MakeShareable - 일반 C++ 포인터로 쉐어드 포인터를 생성한다. MakesShared는 새 오브젝트 인스턴스와 레퍼런스 컨트롤러를 한 메모리 블록에 할당하지만 오브젝트가 public 생성자를 제공해야만 한다. MakeShareable는 덜 효율적 이지만 오브젝트의 생성자가 private이더라도 접근 가능하여 직접 생성하지 않은 오브젝트에 대한 소유권을 가질수 있고, 오브젝트를 소멸시킬 경우에 커스텀 비헤이비어가 지원된다.

    • StaticCastSharedRef 및 StaticCastSharedPtr - 정적인 형변환 유틸리티 함수로, 주로 파생된 타입으로 다운캐스팅 하는데 사용된다.
    • ConstCastSharedRef 및 ConstCastSharedPtr - const 스마트 레퍼런스 또는 스마트 포인터를 mutable 스마트 레퍼런스, 스마트 포인터로 변환한다.

    스마트 포인터 구현 세부

    속도

    스마트 포인터는 특정 하이 레벨 시스템이나 자원 관리 또는 툴 프로그램에 적합하지만 일부 스마트 포인터 타입은 C++ 기본 포인터보다 더 느리며, 이런 오버헤드로 인해 렌더링과 같은 로우레벨 엔진 코드에는 덜 유용하다.

    스마트 포인터의 일반적인 퍼포먼스 이점

    • 모든 연산이 고정비(constant-time)이다.
    • 빌드를 출시할 때, 대부분의 스마트 포인터들을 참조 해제하는 속도가 C++ 기본 포인터만큼 빠르다.
    • 스마트 포인터들을 복사해도 절대 메모리가 할당되지 않는다.
    • 스레드 세이프 스마트 포인터는 잠금 없는 구조이다.

    스마트 포인터의 퍼포먼스 문제점

    • 스마트 포인터의 생성 및 복사는 C++ 기본 포인터의 생성 및 복사 보다 많은 오버헤드가 발생
    • 참조 카운트를 유지하면 기본 연산에 주기가 추가
    • 일부 스마트 포인터는 C++ 기본 포인터보다 메모리 사용량이 더 높다.
    • 레퍼런스 컨트롤러에는 두번의 힙 할당량이 있다. MakeShareable 대신 MakeShared를 사용하면 두번째 할당을 피할 수 있다.

    침범형 접근자(Intrusive Accessors)

    쉐어드 포인터는 비침범형(non-intrusive)으로, 오브젝트가 스마트 포인터의 소유하에 있는지 알수 없다는 뜻이다. 오브젝트를 쉐어드 레퍼런스 또는 쉐어드 포인터로서 접근하려는 경우에는 오브젝트의 클래스를 템플릿 매개변수로 사용하여 TSharedFromThis 에서 오브젝트의 클래스를 파생시켜야한다.

    TSharedFromThis는 AsShared, SharedThis 를 제공하여 오브젝트를 쉐어드 레퍼런스로 변환하고, 쉐어드 레퍼런스를 쉐어드 포인터로 변환한다. 이는 항상 쉐어드 레퍼런스를 반환하는 클래스 팩토리나 쉐어드 레퍼런스, 쉐어드 포인터를 요구하는 시스템에 오브젝트를 넣을때 유용하다.

    class FRegistryObject;
    class FMyBaseClass: public TSharedFromThis<FMyBaseClass>
    {
        virtual void RegisterAsBaseClass(FRegistryObject* RegistryObject)
        {
            // 'this'의 쉐어드 레퍼런스에 접근합니다.
            // <TSharedFromThis>로부터 직접 상속되어 AsShared()와 SharedThis(this)는 동일한 타입을 반환합니다.
            TSharedRef<FMyBaseClass> ThisAsSharedRef = AsShared();
            // RegistryObject는 TSharedRef<FMyBaseClass> 또는 TSharedPtr<FMyBaseClass>를 요구합니다. TsharedRef는 묵시적으로 TsharedPtr로 변환될 수 있습니다.
            RegistryObject->Register(ThisAsSharedRef);
        }
    };
    class FMyDerivedClass : public FMyBaseClass
    {
        virtual void Register(FRegistryObject* RegistryObject) override
        {
            // TSharedFromThis<>로부터 직접 상속되지 않아서 AsShared()와 SharedThis(this)는 각기 다른 타입을 반환합니다.
            // AsShared()는 해당 예제 내 TSharedFromThis<> - TSharedRef<FMyBaseClass>에서 정의된 본래 타입을 반환하게 됩니다.
            // SharedThis(this)는 해당 예제 내 'this' - TSharedRef<FMyDerivedClass>의 타입과 함께 TsharedRef를 반환하게 됩니다.
            // SharedThis() 함수는 ‘this' 포인터와 동일한 범위 내에서만 가능합니다.
            TSharedRef<FMyDerivedClass> AsSharedRef = SharedThis(this);
            // FmyDerivedClass는 FmyBaseClass 타입의 일종이기 때문에 RegistryObject가 TSharedRef<FMyDerivedClass>를 허용합니다.
            RegistryObject->Register(ThisAsSharedRef);
        }
    };
    class FRegistryObject
    {
        // 이 함수는 FmyBaseClass나 그 자녀 클래스에 TsharedRef나 TsharedPtr를 허용합니다.
        void Register(TSharedRef<FMyBaseClass>);
    };
    • AsShared 나 SharedThis를 생성자로 호출하면 쉐어드 레퍼런스가 선언되지 않은 상태이기 때문에 충돌이나 어서트가 발생한다

    형변환

    쉐어드 포인터는 언리얼 스마트 포인터 라이브러리에 포함되어있는 여러가지 지원 함수를 통해 형변환 가능하다. 업캐스팅은 C++포인터와 마찬가지로 묵시적이다. ConstCastSharedPtr 함수로 const cast 연산자를 사용할 수 있으며, StaticCastSharedPtr로 static cast 연산자를 사용할 수 있다. 런타임 타입 정보가 없기 때문에 동적 형변환은 지원되지 않는다.

    // FdragDropOperation가 FAssetDragDropOp이라는 점을 다른 수단을 통해 유효성이 확인되었다고 가정하고 있습니다.
    TSharedPtr<FDragDropOperation> Operation = DragDropEvent.GetOperation();
    // 이제 StaticCastSharedPtr로 형변환할 수 있습니다.
    TSharedPtr<FAssetDragDropOp> DragDropOp = StaticCastSharedPtr<FAssetDragDropOp>(Operation);

    스레드 안정성

    기본적으로 스마트 포인터는 싱글 스레드로 접근하는 것이 안전하지만 멀티 스레드로 접근해야 한다면 스마트 포인터 클래스의 스레드 세이프 버전을 사용하자

    • TSharedPtr<T, ESPMode::ThreadSafe>
    • TSharedRef<T, ESPMode::ThreadSafe>
    • TWeakPtr<T, ESPMode::ThreadSafe>
    • TSharedFromThis<T, ESPMode::ThreadSafe>

    이러한 스레드 세이프 버전은 원자적(atomic)참조 카운팅으로 인해 디폴트보다 다소 느리지만 비헤이비어는 일반 C++ 포인터와 같다.

    • 읽기와 복사본은 항상 스레드 세이프이다.
    • 안정성을 위해 쓰기와 초기화는 반드시 동기화 되어야 한다.

    • 가급적 함수에 데이터를 TSharedRef 또는 TSharedPtr 매개변수로 넣지 않도록 하자 데이터의 해제와 참조 카운팅으로 인행 오버헤드가 발생하게된다. 대안으로 레퍼런스된 오브젝트를 const &으로 넣자
    • 쉐어드 포인터를 불완전한 타입/형식으로 미리 선언할 수 잇따.
    • 쉐어드 포인터는 언리얼 오브젝트와 호환되지 않는다. 언리얼 엔진은 UObject 관리를 위한 별도의 메모리 관리 시스템이 있으며 두 시스템은 다른 시스템이다.

    문서

    반응형

    댓글

    Designed by JB FACTORY