언리얼 C++ 프로그래밍

[언리얼 C++] 스마트 포인터 라이브러리 (2)

언린이 2021. 7. 20. 22:05

언리얼 스마트 포인터 라이브러리에 대한 기본 설명은 [언리얼 C++] 스마트 포인터 라이브러리 (1) (tistory.com) 글을 참고하시기 바랍니다.

이 글에서는 스마트 포인터 라이브러리의 부가적인 기능들에 대해 알아보겠습니다.

 

 

1. 헬퍼 클래스와 함수

언리얼 스마트 포인터 라이브러리는 스마트 포인터를 보다 쉽고 직관적으로 사용할 수 있도록 다양한 헬퍼 클래스와 함수를 제공합니다.

헬퍼 클래스 설명
TSharedFromThis TSharedFromThis에서 클래스를 파생시키면 AsShared 혹은 SharedThis 함수가 추가됩니다. 이러한 함수들을 통해 오브젝트에 대한 TSharedRef를 구할 수 있습니다.
헬퍼 함수 설명
MakeShared 및 MakeShareable 일반 C++포인터로 쉐어드 포인터를 생성합니다.
MakeShared 함수는 새 오브젝트 인스턴스와 레퍼런스 컨트롤러를 한 메모리 블록에 할당하지만, 오브젝트가 public 생성자를 제공해야만 합니다.
MakeShareable 함수는 덜 효율적이지만 오브젝트의 생성자가 private이더라도 접근 가능하여 직접 생성하지 않은 오브젝트에 대한 소유권을 가질 수 있고, 오브젝트를 소멸시킬 경우에는 커스텀 비헤이비어가 지원됩니다.
StaticCastSharedRef  StaticCastSharedPtr 정적인 형변환 유틸리티 함수로, 주로 파생된 타입으로 다운캐스트하는데 사용됩니다.
ConstCastSharedRef  ConstCastSharedPtr const 스마트 레퍼런스 또는 스마트 포인터를 mutable 스마트 레퍼런스 또는 스마트 포인터로 각각 변환합니다.

 

 

2. 스마트 포인터 구현 세부사항

언리얼 스마트 포인터 라이브러리의 모든 스마트 포인터는 기능성 및 효율성 측면에서 일반적인 특징을 공유합니다.

 

- 속도

스마트 포인터를 사용할지 고려할때는 항상 퍼포먼스에 대해서 생각해야 합니다. 스마트 포인터는 특정 하이 레벨 시스템이나 자원 관리 또는 툴 프로그램에 매우 적합하지만 일부 스마트 포인터 타입은 C++ 기본 포인터보다 더 느리며, 이런 오버헤드로 인해 렌더링과 같은 로우 레벨 엔진 코드에는 덜 유용합니다.

 

스마트 포인터의 일반적인 퍼포먼스 이점은 다음과 같습니다.

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

이러한 이점들이 존재하지만, 스마트 포인터의 사용은 퍼포먼스 문제점 또한 동반합니다.

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

 

- 침범형 접근자

쉐어드 포인터는 비침범형으로, 오브젝트가 스마트 포인터의 소유 하에 있는지 알 수가 없습니다. 이런 속성은 일반적으로 문제가 없지만, 오브젝트를 쉐어드 레퍼런스 또는 쉐어드 포인터로서 접근하려는 경우가 있을 수도 있습니다. 이러한 경우에는, 오브젝트의 클래스를 템플릿 매개변수로 사용하여 TSharedFromThis에서 오브젝트의 클래스를 파생시켜야 합니다. TSharedFromThis는 두가지 함수 AsShared 및 SharedThis를 제공하며, 두 함수로 오브젝트를 쉐어드 레퍼런스로 변환하고, 쉐어드 레퍼런스를 또 쉐어드 포인터로 변환할 수 있습니다. 이는 항상 쉐어드 레퍼런스를 반환하는 클래스 팩토리나 쉐어드 레퍼런스 또는 쉐어드 포인터를 요구하는 시스템에 오브젝트를 넣을때 특히나 유용합니다. AsShared 함수는 호출되는 오브젝트의 부모 타입일 수 있는 TSharedFromThis에 템플릿 인자로서 전달된 본래 타입의 클래스를 반환합니다. SharedThis 함수는 this에서 타입을 직접 파생시키고 해당 타입의 오브젝트를 참조하는 스마트 포인터를 반환합니다.

 

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(AsSharedRef);
    }
};
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>

이러한 스레드 세이프 버전은 원자적 참조 카운팅으로 인해 기본보다 다소 느리지만 그 행동은 일반 C++ 포인터와 같습니다.

그래서 두개 이상의 스레드가 포인터에 접근하지 않는다는 것이 확실하다면, 스레드 세이프 버전을 사용하지 않음으로써 퍼포먼스를 향상시킬 수 있습니다.

 

 

3. 제한사항

가급적이면 함수에 데이터를 TSharedRef 또는 TSharedPtr 매개변수로 넣지 않는 것을 권장합니다. 이러한 데이터의 해제와 참조 카운팅으로 인해 오버헤드가 발생하게 됩니다. 그 대안으로 레퍼런스된 오브젝트를 const &로 넣으시기 바랍니다.

 

그리고 쉐어드 포인터를 불완전한 타입/형식으로 미리 선언할 수 있습니다.

 

마지막으로, 쉐어드 포인터는 언리얼 오브젝트(UObject)와 호환되지 않습니다. 언리얼 엔진은 UObject 관리를 위한 별도의 메모리 관리 시스템이 존재하며 스마트 포인터 시스템과는 서로 완전히 다릅니다.