Application/C++

[C++] 캐스트 이해하기 (4) - dynamic_cast

devsalix 2025. 12. 11. 10:51
728x90

1. 들어가기

static_cast랑 reinterpret_cast, const_cast까지 정리해 놓고 나면
마지막으로 자연스럽게 떠오르는 친구가 dynamic_cast다

이름만 보면
“런타임에 뭔가 확인해 준다는데 정확히 뭐가 다른지”
“언제 static_cast 대신 써야 하는지”
헷갈릴 때가 많다

 

이 글에서는

  • dynamic_cast가 정확히 무슨 일을 하는지
  • 언제 static_cast보다 dynamic_cast가 더 어울리는지
  • 실무에서 자주 보게 되는 패턴과 주의할 점

을 한 번에 정리해 본다


2. dynamic_cast가 하는 일

한 줄로 요약하면

런타임에 실제 타입을 확인하면서
상속 관계 안에서 안전하게 캐스트해 주는 도구

 

라고 보면 된다

 

조금 더 풀어서 말하면

  • 다형성 기반 클래스를 가리키는 포인터나 레퍼런스에서
  • 실제 객체 타입을 확인해 보고
    • 맞으면 원하는 타입 포인터로 바꿔 주고
    • 아니면 nullptr 또는 예외를 돌려주는 캐스트

라는 것

 

여기서 중요한 조건이 하나 있다

dynamic_cast가 제대로 동작하려면
기본 클래스에 최소 한 개 이상의 가상 함수가 있어야 한다

 

즉 RTTI(Run-Time Type Information)정보가 있는 다형성 타입이어야 한다는 뜻이다

간단히 말해서

  • struct Base { virtual ~Base() {} }; 처럼
    • vtable이 잡히는 구조라면 dynamic_cast 가능
  • struct Base {}; 처럼
    • 가상 함수가 하나도 없으면 dynamic_cast가 의미 없다

3. 예제 1 안전한 다운캐스트

가장 대표적인 사용처는
“기본 클래스 포인터를 안전하게 파생 클래스 포인터로 바꾸고 싶을 때”다

struct Animal
{
    virtual ~Animal() {}
    virtual void Speak() = 0;
};

struct Dog : Animal
{
    void Speak() override { std::cout << "Woof\n"; }
    void Shake()         { std::cout << "Tail shaking\n"; }
};

struct Cat : Animal
{
    void Speak() override { std::cout << "Meow\n"; }
};

void Foo(Animal* p)
{
    // p가 Dog일 수도 있고 Cat일 수도 있는 상황
    if (auto d = dynamic_cast<Dog*>(p))
    {
        d->Shake();            // Dog인 경우에만 호출
    }
    else
    {
        std::cout << "Dog가 아님\n";
    }
}

 

여기서 만약

Cat* d = static_cast<Cat*>(p);
d->Shake();

 

같이 써 버리면
p가 Cat을 가리키는 순간부터 정의되지 않은 동작으로 바로 빠져 버린다

 

반면 dynamic_cast는

  • 실제 객체가 Dog일 때만 유효한 포인터를 주고
  • 아니면 nullptr를 주기 때문에

런타임에 타입을 안전하게 확인할 수 있다

 

정리하자면

  • 타입이 확실할 때 → static_cast로 다운캐스트 가능
  • 타입이 확실하지 않을 때
    • → 예외 없애고 싶으면 dynamic_cast로 확인 후 처리

라는 패턴으로 가져가면 된다


4. 예제 2 인터페이스 기반 설계에서의 dynamic_cast

실무 코드에서 자주 보이는 또 다른 패턴은
“기본 인터페이스는 같지만
추가 기능이 있는 타입만 골라서 처리하고 싶을 때”다

struct IWidget
{
    virtual ~IWidget() {}
    virtual void Draw() = 0;
};

struct IClickable
{
    virtual ~IClickable() {}
    virtual void OnClick() = 0;
};

struct Button : IWidget, IClickable
{
    void Draw() override    { /* ... */ }
    void OnClick() override { /* ... */ }
};

struct Label : IWidget
{
    void Draw() override    { /* ... */ }
};

void HandleClick(IWidget* w)
{
    // 그려지는 위젯들 중에서
    // 클릭 가능한 것만 골라서 이벤트 처리
    if (auto clickable = dynamic_cast<IClickable*>(w))
    {
        clickable->OnClick();
    }
}

 

여기서 중요한 포인트는

  • 공통 인터페이스는 IWidget이고
  • 클릭 가능한 위젯만 IClickable을 구현

이럴 때

  • 컨테이너에는 전부 IWidget*로 넣어 두고
  • 이벤트 처리 쪽에서만
    • dynamic_cast<IClickable*>로 걸러내서 처리

하는 패턴이 깔끔하게 맞아떨어진다

이런 식으로

  • “기본적으로는 같은 종류의 객체지만
  • 일부 타입만 추가 기능이 있다”

라는 상황에서 dynamic_cast가 꽤 자연스럽게 등장한다


5. 예제 3 포인터가 아닌 레퍼런스에서의 dynamic_cast

dynamic_cast는 포인터뿐만 아니라
레퍼런스에도 쓸 수 있다

다만 레퍼런스 버전은
실패 시 null이 아니라 예외를 던진다는 점이 다르다

void Process(Animal& a)
{
    try
    {
        Dog& d = dynamic_cast<Dog&>(a);
        d.Shake();
    }
    catch (const std::bad_cast& e)
    {
        std::cout << "Dog가 아님\n";
    }
}

 

정리하면

  • dynamic_cast<Dog*>(p)
    • 실패하면 nullptr
    • if 문으로 체크하는 스타일
  • dynamic_cast<Dog&>(ref)
    • 실패하면 std::bad_cast 예외 발생
    • 예외 기반 흐름 제어를 쓸 때 사용

둘 중 어떤 걸 쓸지는
프로젝트 전체가

  • 예외 중심으로 설계되어 있는지
  • 반환값 체크 중심으로 설계되어 있는지

에 따라 통일해 주는 게 좋다


6. 언제 dynamic_cast를 써야 할까 체크리스트

다른 캐스트 글에서와 마찬가지로
dynamic_cast도 “체크리스트”를 통과했을 때만 쓰는 습관이 좋다

아래 질문에 모두 예라고 답할 수 있다면
그때부터 dynamic_cast 후보라고 생각해 보자

 

1 이 타입 계층은 다형성 기반인가

  • 기본 클래스에 최소 하나 이상의 virtual 함수가 있는가

2 지금 내가 하려는 캐스트는 상속 계층 안에서의 변환인가

  • 전혀 관계없는 포인터끼리의 변환이라면 reinterpret_cast 영역에 가깝다

3 컴파일 타임에는 타입을 확신할 수 없고 런타임에 타입을 확인해야 하는 상황인가

  • 컨테이너에 Base*만 잔뜩 넣어 놓고
    실제 타입에 따라 분기해야 하는 경우 등

4 dynamic_cast 실패 시의 처리 방안을 명확히 알고 있는가

  • nullptr 체크
  • std::bad_cast 처리

반대로 아래에 해당한다면
dynamic_cast 대신 다른 선택지를 먼저 고려하는 편이 좋다

  • 타입이 항상 확실한데
    그냥 습관적으로 dynamic_cast를 쓰고 있다
    • → static_cast가 더 간단하고 비용도 적다
  • 상속 설계가 잘못되어
    여기저기서 dynamic_cast를 남발하고 있다
    • → 설계 자체를 손봐야 할 신호일 수 있다

7. 주의할 점

dynamic_cast를 쓸 때 특히 조심해야 할 부분만 따로 뽑아 보면

  • 성능
    • RTTI 정보를 조회하는 작업이라
    • 핫패스에서 남발하면 눈에 띄게 느려질 수 있다
    • 꼭 필요한 지점에만 제한적으로 사용
  • 설계 냄새
    • 코드 전체에 dynamic_cast가 여기저기 흩어져 있다면
    • “진짜 다형성을 잘 활용하고 있는가”를 다시 돌아볼 필요가 있다
    • 대부분의 경우
      • 가상 함수 오버라이드
      • 더 깔끔한 인터페이스 분리
        로 해결할 수 있는 경우가 많다
  • 비 다형성 타입에서의 사용
    • 기본 클래스에 virtual 함수가 없으면
    • 동작 자체가 정의되지 않거나
    • 구현체마다 다르게 행동할 수 있다
    • 반드시 다형성 타입에서만 사용
  • reinterpret_cast 대용으로 쓰지 않기
    • 서로 상속 관계가 전혀 없는 타입 사이 변환은
      dynamic_cast로도 할 수 없다
    • 이런 경우 억지로 해결하려 하면
      설계가 더 꼬이기 쉽다

8. 정리

마지막으로 dynamic_cast를 기억해 둘 만한 문장만 모아 보면

  • dynamic_cast는 다형성 타입 계층 안에서
    런타임 타입을 확인하며 안전하게 캐스트하는 도구다
  • 다운캐스트가 필요하지만
    타입을 100% 확신할 수 없는 상황에서
    nullptr 또는 예외를 통해 실패를 감지할 수 있다
  • 포인터에 쓰면 실패 시 nullptr
    레퍼런스에 쓰면 실패 시 std::bad_cast 예외가 발생한다
  • 기본 클래스에 최소 하나 이상의 virtual 함수가 있어야
    의미 있는 결과를 기대할 수 있다
  • 프로젝트 전체에 dynamic_cast가 너무 자주 등장한다면
    상속 구조나 인터페이스 설계가 꼬였다는 신호일 수 있다

여기까지가 dynamic_cast 편이고
static_cast / reinterpret_cast / const_cast와 같이 묶어서 생각해 두면

  • 컴파일 타임에 확인 가능한 변환은 static_cast
  • 타입 한계를 벗어난 저수준 비트 재해석은 reinterpret_cast
  • const / volatile 수식자만 건드리는 건 const_cast
  • 상속 계층 안에서 런타임 타입 확인이 필요한 변환은 dynamic_cast

라는 큰 그림이 머릿속에 한 번에 그려질 것이다


 

728x90
반응형