언리얼 엔진4에서는 언리얼 스마트 포인터 라이브러리 (Unreal Smart Pointer Library)라는 C++0x 표준 라이브러리와 Boost 스마트 포인터를 본따 언리얼에 어울리는 스마트 포인터를 자체 제작하였다.

UE4에 존제하는 스마트 포인터들은 이러하다.

  • TUniquePtr (유니크 포인터)
  • TSharedPtr (공유 포인터)
  • TWeakPtr (약 포인터)

기본적인 스마트 포인터의 개념은 C++ Smart Pointer 와 동일하다.

추가적으로 언리얼 엔진에는 TSharedRef (공유 레퍼런스)라는 공유포인터용 클래스도 존제한다.

공식 문서에서 설명하는 공유 레퍼런스와 포인터의 장점은 이러하다.

언리얼엔진에서 굳이 커스텀 라이브러리를 만든 이유는 이러하다.

  • 아직 모든 플랫폼에 std::shared_ptr (그리고 tr1::shared_ptr 도) 사용할 수 없다.
  • 모든 컴파일러와 플랫폼에 좀 더 일관된 구현이 가능하다.
  • 다른 언리얼 컨테이너와 유형과 매끄러운 작업이 가능하다.
  • 스레딩이나 최적화를 포함해서 플랫폼 전용 특성에 대해 더 나은 제어가 가능하다.
  • 스레드 안전성 기능을 (퍼포먼스를 위해) 선택적인 것으로 만들고 싶었다.
  • 자체적으로 개선시켰다 (MakeShareable, NULL 에 할당 등).
  • 저희 구현에 예외는 필요치도 바람직하지도 않다.
  • 퍼포먼스에 대한 제어를 강화하고자 했다 (인라이닝, 메모리, virtuals 사용 등).
  • 잠재적으로 디버깅이 더 쉽다 (자유로운 코드 코멘트 등).
  • 가급적 써드 파티 종속성을 늘리고 싶지 않았다.

추가적으로 스마트 포인터를 더욱 쉽고 직관적으로 쓸 수 있도록, 헬퍼 클래스와 함수들이 라이브러리에서 제공되는데 내용은 이러하다.

MakeShareable()은 make_shared와 동일한 기능인 것 같고 그 외에는 캐스팅용 함수들이다.

언리얼 스마트 포인터 라이브러리에 구현되어 있는 여러 유형의 스마트 포인터는 모두 퍼포먼스, 메모리 등의 측면에서 일반적인 특징을 공유한다.

퍼포먼스

공유 포인터(TSharedPtr) 사용을 고려할 때는 항상 퍼포먼스를 염두해야 한다. 일반적으로 꽤 빠르나, 아무데나 쓰는 용도가 아니다. 특정 하이레벨 시스템이나 툴 프로그래밍에는 상당히 탁월한 능력을 발휘하나, Low-Level의 Engine이나 Rendering Path에는 적합하지 않다.

공유 포인터의 일반적인 퍼포먼스 장점과 단점

장점

  • 모든 연산이 고정비(constant-time) 이다.
  • 공유 포인터 레퍼런스 해제가 C++ 포인터만큼 빠르다.
  • 공유 포인터를 복사한다고 절대 메모리가 할당되지 않는다.
  • Thread Safe 버전은 교착상태에 빠지지 않는다(lock-free).
  • Boost 나 STL 에 비하면 구현이 빠르다.

단점

  • 포인터 생성과 복사에 부하가 걸린다.
  • 레퍼런스 카운트 현황을 유지해야 한다.
  • 공유 포인터는 C++ 포인터보다 메모리 사용량이 많다.
  • 레퍼런스 컨트롤러에 힙 메모리 할당량이 추가로 들어간다.
  • 공유 포인터가 몇이든 각 고유 오브젝트마다 하나씩 레퍼런스된다.
  • 약 포인터 (TWeakPtr) 접근은 공유 포인터 접근보다 약간 느리다.

장점과 단점이 극명하기 때문에 신중히 판단하며 사용해야 한다.

모든 공유 포인터 (TSharedPtr, TSharedRef, TWeakPtr) 들은 32 비트로 컴파일할 때 8바이트이며, C++ 포인터와 레퍼런스 컨트롤러 포인터가 uint32로 구성된다.

레퍼런스 컨트롤러 오브젝트는 32 비트로 컴파일 할 경우 12바이트이며 C++ 포인터, 공유 레퍼런스 카운트, 약 레퍼런스 카운트가 uint32로 구성된다.

한 오브젝트를 가리키는 공유포인터나 약포인터는 갯수와 상관없이, 레퍼런스 컨트롤러 오브젝트마다 딱 하나씩만 생성된다.

Thread Safe한 버전의 공유 포인터

보통의 공유 포인터는 싱글 스레드에서만 안전하나, 멀티 스레드에서 접근하도록 해야할 경우엔, 아래의 포인터들을 사용해야한다.

  • TThreadSafeSharedPtr
  • TThreadSafeSharedRef
  • TThreadSafeWeakPtr
  • TThreadSafeSharedFromThis

참조 카운트가 개별적으로(atomic) 변경되기 때문에, 일반 C++ 포인터와 작동 방식은 거의 같다.

몇가지의 팁

  • C++ 포인터를 새로운 공유 포인터로 전달할 때는 보통 “new 연산자”를 사용해야 합니다.
  • 스마트 포인터를 함수 파라미터로 전달할 때는 TWeakPtr 가 아닌, TSharedRef 나 TSharedPtr 를 사용하세요.
  • 스마트 포인터 “thread-safe” 버전은 약간 느립니다. 필요할 때만 사용하세요.
  • 불완전 유형으로의 공유 포인터는, 딱 원하는 대로 미리(forward) 선언할 수 있습니다!
  • 호환 유형의 공유 포인터는 묵시적으로 변환됩니다 (예로 upcasting)
  • 유형을 쉽게 만들려면 TSharedRef< MyClass > 에 대한 typedef 를 만들면 됩니다.
  • 최적의 퍼포먼스를 위해서는 TWeakPtr::Pin 로의 호출(이나 TSharedRef/TSharedPtr 로의 변환)을 최소화시키세요.
  • TSharedFromThis 에서 파생된 클래스는 자신을 공유 레퍼런스로 반환할 수 있습니다.
  • 포인터를 파생된 오브젝트 클래스로 내림변환하려면, StaticCastSharedPtr() 함수를 사용하세요.
  • const 오브젝트는 공유 포인터와 완벽 지원됩니다!
  • ConstCastSharedPtr() 함수를 사용하여 const 공유 포인터를 mutable 로 만들 수 있습니다.
  • 항상 깊은 스택 프레임의 C++ 레퍼런스로 변환하세요. 공유 포인터는 멤버 레퍼런스에나 최적이지, 임시 스택에는 아닙니다.
  • C++ 포인터와는 달리 공유 포인터는 memcpy 가 불가능하므로, 공유 포인터 배열을 사용할 때는 이 점을 고려하시기 바랍니다.

그리고 한계

  • 공유 포인터는 Unreal 오브젝트(UObject 클래스)와 호환되지 않습니다!
  • 동적으로 할당되는 배열 (예로 (MakeSharable( new in32[20] ) 같은 식)은 아직 지원되지 않습니다.
  • TSharedPtr/TSharedRef 에서 Bool 로의 묵시적 변환은 지원되지 않습니다.