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;

언리얼 엔진 스마트 포인터 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 파생 클래스와 작업할 때는 언리얼 엔진의 가비지 컬렉션 시스템과의 상호작용을 이해하고 적절히 대응해야 합니다.

 최적의 메모리 관리를 위해서는 프로젝트의 요구사항을 잘 이해하고, 각 상황에 가장 적합한 스마트 포인터 유형을 선택하는 것이 중요합니다. 지속적인 프로파일링과 코드 리뷰를 통해 스마트 포인터 사용의 효과성을 모니터링하고 최적화하는 것이 좋습니다.