icon안동민 개발노트

스마트 포인터 개요


 스마트 포인터는 C++에서 메모리 관리를 자동화하고 메모리 누수를 방지하는 강력한 도구입니다.

 언리얼 엔진은 자체적인 스마트 포인터 구현을 제공하여 게임 개발에서의 메모리 관리를 더욱 효율적으로 만듭니다.

스마트 포인터의 필요성

  1. 메모리 누수 방지
  2. 자원 해제의 자동화
  3. 소유권 관리 용이
  4. 예외 안전성 향상

기본 개념

 소유권 (Ownership) : 객체의 수명을 관리하는 책임을 가리킵니다. 스마트 포인터는 이 소유권을 명시적으로 표현합니다.

 참조 카운팅 (Reference Counting) : 객체를 참조하는 포인터의 수를 추적하여, 더 이상 참조되지 않을 때 객체를 자동으로 삭제합니다.

주요 스마트 포인터 유형

 1. unique_ptr

  • 독점적 소유권을 가짐
  • 복사 불가, 이동만 가능
  • 가장 가벼운 오버헤드
##include <memory>
 
std::unique_ptr<int> ptr = std::make_unique<int>(10);
// ptr을 통해서만 접근 가능

 2. shared_ptr

  • 공유 소유권
  • 참조 카운팅 사용
  • 순환 참조 문제 가능성 있음
std::shared_ptr<int> ptr1 = std::make_shared<int>(20);
std::shared_ptr<int> ptr2 = ptr1; // 참조 카운트 증가

 3. weak_ptr

  • shared_ptr과 함께 사용
  • 순환 참조 문제 해결
  • 객체의 존재 여부 확인 가능
std::weak_ptr<int> weakPtr = ptr1;
if (auto sharedPtr = weakPtr.lock()) {
    // 객체가 아직 존재함
}

언리얼 엔진의 스마트 포인터 구현

 언리얼 엔진은 자체적인 스마트 포인터 구현을 제공합니다.

 1. TSharedPtr

  • std::shared_ptr와 유사하지만 언리얼 엔진에 최적화되어 있습니다.
TSharedPtr<FMyClass> Ptr = MakeShared<FMyClass>();

 2. TUniquePtr

  • std::unique_ptr와 유사한 기능을 제공합니다.
TUniquePtr<FMyClass> Ptr = MakeUnique<FMyClass>();

 3. TWeakPtr

  • std::weak_ptr와 유사한 기능을 제공합니다.
TWeakPtr<FMyClass> WeakPtr = Ptr;

UE 스마트 포인터 vs C++ 스마트 포인터

  1. 성능 최적화 : 언리얼 엔진의 스마트 포인터는 게임 개발에 특화된 최적화가 적용되어 있습니다.
  2. 가비지 컬렉션 통합 : 언리얼 엔진의 가비지 컬렉션 시스템과 잘 통합됩니다.
  3. 언리얼 객체 모델 지원 : UObject 파생 클래스와의 호환성이 뛰어납니다.

메모리 누수 방지 기법

  1. RAII (Resource Acquisition Is Initialization) 원칙 적용
  2. 명시적인 소유권 관리
  3. 순환 참조 방지

 예시

class FResourceManager
{
    TSharedPtr<FResource> Resource;
 
public:
    FResourceManager() : Resource(MakeShared<FResource>()) {}
    // 소멸자에서 명시적 해제 불필요
};

순환 참조 문제 해결

 TWeakPtr를 사용하여 순환 참조를 방지할 수 있습니다.

class FParent
{
public:
    TSharedPtr<FChild> Child;
};
 
class FChild
{
public:
    TWeakPtr<FParent> Parent; // 약한 참조 사용
};

성능 고려사항

  1. 참조 카운팅 오버헤드 : TSharedPtr 사용 시 참조 카운팅으로 인한 약간의 성능 저하가 있을 수 있습니다.
  2. 메모리 사용 : 스마트 포인터는 추가 메모리를 사용합니다. (컨트롤 블록 등)
  3. 캐시 효율성 : 포인터 역참조로 인한 캐시 미스 가능성이 있습니다.

 최적화 팁

  • 가능한 경우 TUniquePtr 사용
  • 불필요한 TSharedPtr 복사 피하기
  • 핫 경로에서 참조로 전달하기

언리얼 엔진 객체 생성 및 관리

 언리얼 엔진에서 스마트 포인터를 효과적으로 활용하는 방법

  1. 액터가 아닌 객체 관리
UPROPERTY()
TSharedPtr<FMyComplexObject> ComplexObject;
 
// 생성자에서
ComplexObject = MakeShared<FMyComplexObject>();
  1. 컴포넌트 참조 관리
UPROPERTY()
TWeakPtr<UMyComponent> WeakComponentRef;
 
// BeginPlay에서
WeakComponentRef = Cast<UMyComponent>(GetComponentByClass(UMyComponent::StaticClass()));
  1. 비동기 작업 관리
TSharedPtr<FAsyncTask> AsyncTask = MakeShared<FAsyncTask>();
AsyncThread = FRunnableThread::Create(AsyncTask.Get(), TEXT("AsyncThread"));

주의사항

  1. UObject 파생 클래스에는 스마트 포인터 대신 UPROPERTY 사용
  2. 순환 참조 주의 (특히 델리게이트 사용 시)
  3. 스마트 포인터의 스레드 안전성 고려 (TSharedPtr은 스레드 안전하지 않음)

Best Practices

  1. 명확한 소유권 정의
TSharedPtr<FMyClass> OwnedObject; // 이 클래스가 소유
TWeakPtr<FMyClass> ReferencedObject; // 다른 곳에서 소유
  1. 함수 매개변수로 전달 시 참조 사용
void ProcessObject(const TSharedPtr<FMyClass>& Object)
{
    // 참조로 전달하여 불필요한 참조 카운트 증가 방지
}
  1. 초기화 시점 주의
UPROPERTY()
TSharedPtr<FMyClass> MyObject;
 
void AMyActor::BeginPlay()
{
    Super::BeginPlay();
    MyObject = MakeShared<FMyClass>(); // BeginPlay에서 초기화
}
  1. 가능한 경우 TUniquePtr 사용
TUniquePtr<FMyClass> UniqueObject = MakeUnique<FMyClass>();
  1. 디버깅을 위한 사용자 정의 삭제자
auto Deleter = [](FMyClass* Ptr) 
{
    UE_LOG(LogTemp, Warning, TEXT("Object deleted"));
    delete Ptr;
};
TSharedPtr<FMyClass, decltype(Deleter)> DebugPtr(new FMyClass(), Deleter);

 스마트 포인터는 C++ 및 언리얼 엔진 개발에서 메모리 관리의 복잡성을 크게 줄여줍니다.

 언리얼 엔진의 스마트 포인터 구현은 게임 개발 환경에 최적화되어 있어, 엔진의 다른 기능들과 잘 통합됩니다.

 그러나 스마트 포인터 사용 시 성능 영향과 특정 상황에서의 제한사항을 항상 고려해야 합니다.

 특히 UObject 파생 클래스와 작업할 때는 언리얼 엔진의 가비지 컬렉션 시스템과의 상호작용을 이해하고 적절히 대응해야 합니다.