C++, CS

[C++] RTTI와 RAII 정리

dhlee-dev 2026. 4. 24. 20:39

이전에 가상 함수 호출이 내부적으로 vtablevptr을 통해 이루어진다는 것을 알 수 있었다.

가상 함수 호출은 런타임에 객체 내부의 vptr을 통해 vtable을 참조하고,
그 안에 저장된 함수 주소를 찾아 실제로 호출할 함수를 결정하는 방식이었다.

여기서 다음과 같은 궁금증이 생겼다.
런타임에 이 객체가 실제로 어떤 타입인지 어떻게 알 수 있을까?

이번에는 이 궁금증을 바탕으로
런타임에 객체의 타입 정보를 확인하거나 안전하게 형변환할 수 있게 해주는 RTTI에 대해 정리해보려고 한다.

이어서 이렇게 생성하고 사용하는 객체나 자원을
어떻게 더 안전하게 관리할 수 있는지와 관련된 RAII 개념도 함께 정리해보려고 한다.


RTTI(Run Time Type Information), 런타임 형식 정보란?

RTTI는 프로그램 실행 중에 개체의 형식이 결정될 수 있도록 하는 메커니즘이다.
이를 이용해 런타임에 객체의 실제 타입 정보를 확인하거나, 그 정보로 안전한 형변환이 가능하다.

RTTI와 관련된 대표적인 요소는 다음과 같다.

1. typeid

typeid는 타입 정보를 얻는 연산자이다.

다형성을 가진 클래스라면 typeid를 통해 객체의 실제 동적 타입을 식별할 수 있다.
반대로 다형성을 가지지 않는 클래스라면 정적 타입을 기준으로 타입 정보가 결정된다.

또한 typeid는 포인터 자체를 넣는지 포인터를 역참조한 객체를 넣는지에 따라 결과가 달라진다.

  • typeid(p) : 포인터 변수 자체의 타입
  • typeid(*p) : 포인터가 가리키는 객체의 타입

2. type_info

type_info는 타입 정보를 담고 있는 클래스이다.
typeid 연산의 결과로 type_info 객체에 대한 참조를 얻을 수 있다.

  • typeid : 타입 정보를 얻는 연산자
  • type_info : 타입 정보를 담고 있는 객체

라고 생각하면 된다.

보통 type_info 정보는 vtable 안에 있거나,
vtable과 연결된 정적 영역에 위치하는 방식으로 구현된다고 알려져 있다.

3. dynamic_cast

dynamic_cast는 런타임 타입 검사를 이용해 안전하게 형변환을 수행하는 연산자이다.

주로 다형성을 가진 클래스를 안전하게 다운캐스팅할 때 사용하며,
다중 상속 구조에서는 크로스 캐스팅에도 사용할 수 있다.

dynamic_cast는 객체의 실제 타입과 상속 관계를 확인한 뒤,
변환 가능한 경우에만 형변환을 수행한다.

변환에 실패하면 결과는 다음과 같다.

  • 포인터 변환 실패 → nullptr
  • 레퍼런스 변환 실패 → std::bad_cast 예외

또한 부모 클래스에는 없고 자식 클래스에만 있는 전용 기능을 사용해야 할 때,
dynamic_cast를 이용해 해당 자식 타입인지 확인한 뒤 안전하게 사용할 수도 있다.


RTTI 테스트 코드

아래 코드는 다형성을 가진 클래스의 typeid, 다형성을 가지지 않은 클래스의 typeid,
dynamic_cast 성공 / 실패, 그리고 특정 자식 클래스일 때만 동작을 수행하는 예시를 확인하기 위한 코드이다.

#include <iostream>
#include <typeinfo>
#include <memory>

using namespace std;

// typeid 확인
class Base {
public:
    virtual ~Base() = default;
    virtual void f() {}
};

class Derived : public Base {};

// 다형성이 없는 클래스 typeid 확인
class A {
public:
    void f() {}
};

class B : public A {};


// dynamic_cast 확인
class Warrior : public Base {
public:
    void Slash() { cout << "Slash\n"; }
};

class Mage : public Base {
public:
    void Cast() { cout << "Cast\n"; }
};

void Act(Base* character) 
{
    if (Warrior* warrior = dynamic_cast<Warrior*>(character)) {
        warrior->Slash();
    }

    if (Mage* mage = dynamic_cast<Mage*>(character)) {
        mage->Cast();
    }
}

int main()
{
    // 다형성을 가진 클래스 typeid 로 확인
    Base* p = new Derived;
    unique_ptr<Base> p2 = make_unique<Derived>();

    cout << typeid(p).name() << "\n";
    cout << typeid(*p).name() << "\n";
    cout << typeid(p2).name() << "\n";
    cout << typeid(*p2).name() << "\n";
    cout << "\n";

    // 다형성을 가지지 않은 클래스 확인
    B b;
    A* test = &b;
    cout << typeid(test).name() << "\n";
    cout << typeid(*test).name() << "\n";
    cout << "\n";


    // dynamic_cast 시도
    Base* ptr = new Derived;
    Derived* d = dynamic_cast<Derived*>(ptr);
    if (d) {
        cout << "dynamic cast!\n";
    }
    else {
        cout << "dynamic cast failed...\n";
    }

    Base* ptr2 = new Base;
    Derived* d2 = dynamic_cast<Derived*>(ptr2);
    if (d2) {
        cout << "dynamic cast!\n";
    }
    else {
        cout << "dynamic cast failed...\n";
    }
    cout << "\n";


    // dynamic_cast로 특정 클래스 동작 수행
    Base* warrior = new Warrior();
    Act(warrior);

    Base* mage = new Mage();
    Act(mage);


    delete p;
    delete ptr;
    delete ptr2;
    delete warrior;
    delete mage;
}

결과 예시

class Base * __ptr64
class Derived
class std::unique_ptr<class Base,struct std::default_delete<class Base> >
class Derived

class A * __ptr64
class A

dynamic cast!
dynamic cast failed...

Slash
Cast

코드에서 확인할 수 있는 점

1. typeid(p)typeid(*p)는 다르다

pBase* 타입의 포인터이다.
따라서 typeid(p)는 포인터 변수 자체의 타입인 Base*를 확인한다.

반면 typeid(*p)는 포인터가 가리키는 객체를 확인한다.
Base에는 가상 함수가 있으므로 다형성을 가진 클래스이고,
이 경우 실제 객체 타입인 Derived를 확인할 수 있다.

2. 스마트 포인터 또한 구분이 가능하다

코드에서 p2unique_ptr<Base> 타입이다.

따라서 typeid(p2)unique_ptr<Base> 타입을 확인한다.

반면 typeid(*p2)는 스마트 포인터가 가리키는 객체를 확인하므로,
실제 객체 타입인 Derived를 확인할 수 있다.

3. 다형성을 가지지 않은 클래스는 정적 타입을 기준으로 본다

A* test = new B; 에서 실제 객체는 B이다.

하지만 A에는 가상 함수가 없으므로 다형성을 가진 클래스가 아니다.

따라서 typeid(*test)를 해도 실제 객체 타입인 B가 아니라
정적 타입인 A를 기준으로 타입 정보가 결정된다.

즉, typeid로 실제 동적 타입을 얻으려면 기반 클래스가 다형성을 가져야 한다.

4. dynamic_cast는 성공할 수도 있고 실패할 수도 있다

Base* ptr = new Derived;
Derived* d = dynamic_cast<Derived*>(ptr);

이 경우 p가 실제로 Derived 객체를 가리키고 있으므로
캐스팅에 성공한다.

반면,

Base* ptr2 = new Base;
Derived* d2 = dynamic_cast<Derived*>(ptr2);

이 경우 실제 객체는 Base이므로
Derived*로 안전하게 변환할 수 없다.

따라서 결과는 nullptr가 된다.

5. 특정 자식 타입일 때만 동작을 수행할 수 있다

Act 함수에서는 Base*를 인자로 받지만 내부에서 dynamic_cast를 이용해 실제 타입을 확인한다.

void Act(Base* character)
{
    if (Warrior* warrior = dynamic_cast<Warrior*>(character)) {
        warrior->Slash();
    }

    if (Mage* mage = dynamic_cast<Mage*>(character)) {
        mage->Cast();
    }
}

이렇게 하면 실제 객체가 Warrior일 때는 Slash()를 호출하고,
Mage일 때는 Cast()를 호출할 수 있다.

즉, 부모 클래스 포인터로 객체를 받더라도
특정 자식 타입일 때만 전용 동작을 수행하도록 만들 수도 있다.


RAII(Resource Acquisition Is Initialization)란?

RAII는 자원의 수명을 객체의 수명에 맞춰 관리하자는 것이다.
이는 생성자에서 자원을 획득하고, 소멸자에서 자원의 해제가 되도록 하자는 것을 의미한다.

RAII는 메모리뿐만 아니라 파일, 락, 소켓 같은 다양한 자원 관리에 사용할 수 있다.

RAII의 대표적인 예시로 스마트 포인터, lock_guard 등이 있다.


RAII의 장점

RAII의 장점은 다음과 같다.

  • 자원 해제 시점이 스코프 종료 시점으로 명확하다.
  • 예외가 발생하거나 함수가 중간에 종료되어도 자원을 해제할 수 있다.
  • delete, free, unlock 같은 해제 코드를 직접 호출하지 않는 실수를 줄여준다.

정리

RTTI

RTTI는 런타임에 객체의 실제 타입 정보를 확인하거나,
안전한 형변환을 가능하게 해주는 메커니즘이다.

대표적으로 다음과 같은 요소가 있다.

  • typeid
  • type_info
  • dynamic_cast

typeid는 타입 정보를 확인하는 데 사용한다.
type_info는 타입 정보를 담고 있는 클래스이다
dynamic_cast는 실제 타입을 검사하면서 안전하게 형변환하는 데 사용한다.

RAII

RAII는 자원의 수명을 객체의 수명에 맞추는 방식이다.
생성자에서 자원을 획득하고, 소멸자에서 자원을 해제하도록 만들어 자원을 안정적으로 관리할 수 있다.


마무리

이번에 RTTI를 정리하면서 typeiddynamic_cast를 통해
런타임에 객체의 타입 정보를 확인하거나 안전하게 형변환할 수 있다는 것을 알게 되었다.

또한 RAII를 정리하면서 모던 C++에서는 객체의 수명을 이용해
자원을 안전하게 관리한다는 개념도 이해할 수 있었다.

이전에는 C++ 문법을 사용하는 데 집중했다면,
이제는 실제 동작 방식이나 내부 처리를 함께 정리하면서
C++에 대한 이해가 조금씩 더 깊어지고 있다고 생각한다.