
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
라는 큰 그림이 머릿속에 한 번에 그려질 것이다
'Application > C++' 카테고리의 다른 글
| [C++] 캐스트 이해하기 (3) - const_cast (0) | 2025.12.10 |
|---|---|
| [C++] 캐스트 이해하기 (2) - reinterpret_cast (0) | 2025.12.09 |
| [C++] 캐스트 이해하기 (1) - static_cast (0) | 2025.12.08 |
| [C++] 숫자 쉼표(금액) 표시 하기 (Comma) (0) | 2023.11.08 |
| [C++] 2중 포인터 동적 할당 (0) | 2023.03.16 |