4 분 소요

RTTI (Run-Time Type Information)

런타임에 객체의 실제 타입을 파악할 수 있도록 하는 C++ 매커니즘
컴파일 시간이 아닌 ‘실행 중’(런타임)에 타입을 식별할 수 있게 해준다

RTTI 지원 기능

  • dynamic_cast
    • 다만 아래의 VTable이 추가로 필요
    • 그렇기에 virtual 키워드가 들어간 가상함수를 포함한 클래스여야 제대로 동작
    • VTable이 없다면 업캐스팅 같은 VTable을 사용하지 않는 경우만 제대로 동작
      (캐스팅 관련하여 지난번에 다룬 포스팅)
Base* b = new Derived();
Derived* d = dynamic_cast<Derived*>(b); // OK, 다만 VTable이 없다면 '다형 형식'이 아니라는 컴파일 에러(C2683)
  • typeid 연산자
    • C++에서 객체의 실제 타입을 반환함
      (아래의 type_info 반환)
    • typeid(TypeName)인 경우는 컴파일 시간에 판단
    • typeid(*ptr)인 경우는 런타임 중 판단
      (VTable + Virtual 함수이 없다면 제대로 동작하지 않음)
      • 정확히는 ‘정적 타입’만을 체크 (연산 자체는 런타임 이긴 함)
      • nullptr 같은 것을 넣으면 bad_typeid 예외 발생
  • std::type_info
    • RTTI 정보를 저장하는 타입
    • 전역적으로 하나의 type_info 개체를 생성하기에 == 연산 비교가 매우 빠름
    • RTTI 옵션을 키는 경우, VTable에 type_info 정보가 추가로 저장된다
#include <typeinfo>

Base* b = new Derived();

const std::type_info& info = typeid(*b);

std::cout << info.name() << '\n';

VTable(가상 함수 테이블) 과의 연관성

  • VTable 자체는 ‘다형성’을 지원하기 위한 기능으로
    실제 객체가 호출할 ‘함수 주소’를 찾기 위한 테이블

  • RTTI 옵션을 키면 VTable에 RTTI 용 typeinfo 포인터가 추가
    (옵션을 끄면 RTTI에서 typeinfo 블록을 가리키는 항목 제거)

  • 그렇기에 RTTI 옵션을 키더라도
    VTable + Virtual 키워드를 통한 가상함수를 포함해야
    제대로 동작하므로 유의하자

    • VTable을 사용할 수 없으면 ‘정적 타입’ 검사만 가능해짐
[vtable]
  ├─ entry#0  &typeinfo for Derived
  ├─ entry#1  &Derived::FuncA
  ├─ entry#2  &Derived::FuncB
  • 정적 타입 검사?
    • 컴파일 타임에 선언된 ‘타입’을 기준으로만 판단함
      (코드에 적힌 타입 자체를 기준으로 판단)
      (업캐스팅의 경우, 컴파일 타임에 충분히 검사가 가능)
      (그러나 다운 캐스팅의 경우는 타입 확인이 필요하기에 x)
// 업 캐스팅 - 자식 클래스의 객체(포인터)를 부모 클래스 타입으로 가리킴
Base* b = new Derived();

// 다운 캐스팅 - 부모 클래스 타입의 포인터(객체)를 자식 클래스 타입으로 가리킴
Drived* d = dynamic_cast<Drived*>(b);

RAII (Resource Acquisition Is Initialization)

(관련한 예전 포스팅)

객체가 생성될 때, 자원을 획득하고(Initialization)
객체가 스코프를 벗어나며 소멸될 때, 자원을 반환(Resource Release)하는 패턴

자원의 소유권을 ‘객체’에 귀속시킨다

  • 생성자에서 자원을 얻고
    소멸자에서 자원을 반환
    (메모리/리소스의 생명 주기를 ‘객체’의 생명주기에 묶어 자동으로 관리)

  • 메모리, 파일 핸들 등 다양한 ‘자원’을 획득한 후
    해제를 잊어버리면 치명적인 문제가 발생하기에 이를 예방하기 위함

  • C++에서 ‘변수’가 스코프를 벗어나면 ‘소멸자’를 자동으로 호출하는 것이 이를 위한 것
    • 예외가 발생하여도 소멸자 호출
    • 생성자/소멸자, 스코프 기반 수명, 예외 안정성, 소유권 개념 을 바탕으로 한다
  • 이에 적합한 개념이 바로 ‘스마트 포인터

스마트 포인터

일반 포인터처럼 동작하나,
메모리를 자동으로 반환하는 클래스 템플릿

  • RAII 패턴에 기반하여 ‘스코프’를 벗어남에 기반하여 작동한다
  • 일반 Row Pointer의 메모리 누수, 예외 안정성, 다중 포인터의 이중 삭제 등을 예방 가능
  • ‘소유권’ 개념에도 적합한 기능
    (메모리 해제 책임)

Unique_ptr (유니크 포인터)

  • 한 객체의 소유자는 ‘하나’라는 개념
    (단일 소유)

  • 소유권의 ‘이동’(Move)는 가능하나 ‘복사’는 불가능
    (Move와 rvalue에 대하여?)

  • Scope를 벗어나면 Delete됨

특징

  • 가장 안전하고 빠름(다른 스마트 포인터들은 몇가지 확인 등이 필요하므로)
  • 명확한 소유권 (독점적인 소유)
  • 정리 개념이 확실 (Shared_ptr의 RefCount 개념이 없음)
std::unique_ptr<Foo> p = std::make_unique<Foo>(); // Scope 벗어날시 메모리 해제

Shared_ptr

  • 한 객체를 ‘여럿’이서 소유
    (공동 소유)

  • 별도의 제어 블록을 가지며
    이곳에 ‘참조 카운트’를 저장
    (추가적인 메모리 공간)

  • 참조될때마다 참조 카운트가 증가하며
    참조하는 대상이 사라질때 참조 카운트 제거

    • 0이 되었을 때, 자원을 회수함
auto sp1 = std::make_shared<Foo>();
auto sp2 = sp1; // 같은 객체 공유 (참조 카운터 + 1)

특징

  • 강한 참조(Shared_ptr)와 약한 참조(Weak_Ptr)이 나뉘며
    강한 ‘참조’만 참조 카운트에 포함
  • 일반 포인터 / Unique_ptr 보단 느림
    (그렇게 신경쓸 정돈 아니지만 알아는 두기)
  • 순환 참조의 문제 존재
    • 각각의 객체가 서로를 Shared_ptr로 가리키면
      ‘강한 참조’가 계속 1인 상태로 남아 있어
      메모리 해제가 되지 않는 현상
// TSharedPtr<> 은 Unreal 의 Shared_ptr
class Parent
{
public:
    TSharedPtr<Child> MyChild; // 부모 → 자식 (강한 참조)
};

class Child
{
public:
    TSharedPtr<Parent> MyParent; // 자식 → 부모 (강한 참조)
};

void CreateCircularReference()
{
    TSharedPtr<Parent> parent = MakeShared<Parent>();
    TSharedPtr<Child> child = MakeShared<Child>();

    parent->MyChild = child;   // 자식 참조 카운트 +1
    child->MyParent = parent;  // 부모 참조 카운트 +1
}

Weak_ptr

  • Shared_ptr을 ‘관찰’하지만 ‘소유’하진 않음
    (비소유)

특징

  • ‘약한’ 참조 카운트를 증가시키기에
    ‘순환 참조’를 일으키지 않는다
    • Shared_ptr의 강한 참조 카운트가 0이 된 순간 메모리 해제
    • 그렇기에 Weak_Ptr은 ‘Shared_Ptr’이 아직 존재하는지를
      체크하고 사용하는 것이 권장됨
      (C++ : lock, Unreal : Pin)
// AsShared()를 사용하려면 TSharedFromThis를 상속받아야 함
class UIWidget : public TSharedFromThis<UIWidget>
{
private:
    TWeakPtr<UIWidget> ParentWidget;       // 자식 → 부모 : 약한 참조 ✅
    TArray<TSharedPtr<UIWidget>> Children; // 부모 → 자식 : 강한 참조 유지

public:
    void NotifyParent(FString Message)
    {
        // 1. 부모가 아직 살아있는지 확인
        TSharedPtr<UIWidget> Parent = ParentWidget.Pin();

        if (Parent.IsValid())
        {
            // 2. Pin 성공 → 부모 안전하게 접근
            Parent->ReceiveMessage(Message);
            UE_LOG(LogTemp, Log, TEXT("부모에게 메시지 전달: %s"), *Message);
        }
        else
        {
            // 3. 부모가 이미 삭제됨
            UE_LOG(LogTemp, Warning, TEXT("부모가 없어 메시지 전달 실패"));
        }
    }

    void AddChild(TSharedPtr<UIWidget> Child)
    {
        Children.Add(Child);                  // 부모 → 자식 : 강한 참조
        Child->ParentWidget = AsShared();     // 자식 → 부모 : 약한 참조 (TWeakPtr에 대입하면 자동 변환)
        // ✅ 순환 참조 없음! 부모 삭제 시 자식도 자연스럽게 정리
    }
};

스마트 포인터 정리 표

종류 소유권 특징 장점 단점 대표 사용
unique_ptr 단독 소유 복사 불가, 이동만 가능 빠르고 안전, delete 자동 공유 불가 단일 소유 리소스
shared_ptr 공유 소유 참조 카운트 기반 여러 곳에서 사용 가능 성능 저하, 순환 참조 위험 공동 소유 객체
weak_ptr 비소유 shared_ptr 감시 순환 참조 해결 lock 필요, nullptr 가능성 shared_ptr 보조 참조

TMI - 정적 & 동적 바인딩

바인딩 종류 결정 시점 사용됨 대표 예
정적 바인딩 (Static Binding) 컴파일 타임 일반 함수, 정적 함수, 비가상 함수 오버로딩, non-virtual 함수
동적 바인딩 (Dynamic Binding) 런타임 virtual 함수 가상 함수 override 호출
  • 컴파일 타임의 비 가상함수, inline, 오버로딩 등이 정적 바인딩
  • Virtual 키워드 + VTable이 대표적인 동적 바인딩

댓글남기기