20. RTTI: dynamic_cast / typeid 의 내부 동작
난이도 상내 리뷰 · C++
해설 · C++
해설 — RTTI: dynamic_cast / typeid 의 내부 동작
난이도: 상
요약
다형적 타입은 vtable 을 통해 실행 시점의 실제 타입 정보(type_info) 에 접근한다. typeid(*e) 는 이 정보를 읽어 런타임 타입을 비교하고, dynamic_cast 는 상속 그래프를 따라 목표 서브오브젝트를 찾아 포인터를 보정하며, 실패 시 포인터 형은 nullptr, 참조 형은 std::bad_cast 예외로 갈린다. 예상 출력:
1: 1 // typeid(*e) == typeid(Boss) → 실제 타입은 Boss
2: 0 // typeid(*e) == typeid(Monster) → 정확 일치만 참(파생≠기저)
3: 1 // dynamic_cast<Monster*>(e) → Boss is-a Monster, 성공
4: 0 // dynamic_cast<Character*>(e) → Boss 는 Character 아님, nullptr
5: 1 // dynamic_cast<void*>(e) → 최파생 객체(Boss) 주소
6: bad_cast // dynamic_cast<Character&>(*e) → 참조 실패는 예외
7: 1 // dynamic_cast<Collidable*>(r) → 형제 베이스 cross-cast 성공
8: 1 // typeid(*npp) == typeid(NonPoly) → 비다형은 정적 타입으로 결정
핵심 개념
type_info: 각 타입마다 하나 존재하는 RTTI 서술자.typeid는 이것의 참조를 준다.==비교,name(),before(),hash_code()제공. 다형적 타입의type_info포인터는 vtable 안(보통 슬롯 −1, 구현 정의) 에 저장된다.- 다형 vs 비다형: 가상 함수가 하나라도 있으면 다형적 → 런타임 타입 조회 가능. 없으면
typeid/dynamic_cast는 정적 타입으로 컴파일타임에 결정(런타임 조회 없음). dynamic_cast: 다운캐스트/사이드캐스트를 런타임에 안전하게 수행. 실제 객체의 상속 구조를 조회해 목표 타입 서브오브젝트가 있으면 그 위치로 포인터를 보정, 없으면 실패.
단계별 내부 동작
typeid(*e)(라인 1,2):e는 다형적이므로*e의 vtable 을 찾아 그 안의type_info*를 읽는다. 그type_info와typeid(Boss)/typeid(Monster)의type_info를 비교.Boss객체이므로Boss와만 일치(정확 타입 일치). "Monster 이기도 하다" 는 참이 아님 —typeid는 is-a 가 아니라 정확 동일성을 본다.- 포인터
dynamic_cast(라인 3,4): 런타임이*e의 실제 타입(Boss)에서 목표(Monster/Character)로 가는 경로를 상속 그래프에서 찾는다.Boss→Monster→Entity경로에Monster가 있으니 성공(포인터를Monster서브오브젝트로 보정, 단일 상속이라 여기선 오프셋 0).Character는 경로에 없으니nullptr. dynamic_cast<void*>(라인 5): 특수 규칙 — 다형적 포인터를void*로 dynamic_cast 하면 최파생(most-derived) 객체의 시작 주소를 준다. 다중 상속에서 기저 포인터가 객체 중간을 가리켜도 이걸로 실제 객체 머리를 복원할 수 있다.- 참조
dynamic_cast실패 (라인 6): 포인터는 실패를nullptr로 표현할 수 있지만 참조에는 "널 참조" 가 없다. 그래서 참조 캐스트 실패는std::bad_cast예외로 알린다. - cross-cast (라인 7):
GameObject : Renderable, Collidable.Renderable*에서 형제 베이스Collidable*로의 변환은static_cast로는 불가능(상속 관계가 아님)하지만,dynamic_cast는 실제 객체(GameObject)를 조회해Collidable서브오브젝트를 찾아 포인터를 그 오프셋으로 보정해 성공. 두 서브오브젝트 주소는 서로 다르다. - 비다형
typeid(라인 8):NonPoly는 가상 함수가 없어typeid(*npp)는*npp를 실제로 역참조하지 않고 정적 타입NonPoly로 컴파일타임 결정. (그래서npp가 널이어도 UB 가 아니라는 점이 다형 타입과 다르다.)
흔한 오해·함정
- "
typeid는 is-a 를 검사한다" → 아니다. 정확 타입 일치만 참(라인 2). is-a 판정은dynamic_cast로. - 비다형 타입에서 런타임 타입을 기대 → 가상 함수가 없으면
typeid/dynamic_cast는 정적 타입.void*나 슬라이스된 값에서 원 타입 복원 불가. - 참조 실패를 널 체크로 잡으려 함 → 참조
dynamic_cast실패는 예외다.try/catch(std::bad_cast)필요. - 성능 오해:
dynamic_cast는 상속 그래프 탐색이라 비용이 있다(특히 깊은/다중 상속). 핫 패스에서 남발하면 느리다 → 가상 함수 디스패치나 태그/방문자(Visitor)로 대체 고려. - 비교는
type_info::operator==로:name()문자열 비교는 이식성·성능 모두 나쁨.==또는hash_code()/type_index사용.
면접 포인트
typeid(정확 타입 동일성) vsdynamic_cast(is-a + 포인터 보정)의 차이, 그리고 다형/비다형에서 각각이 컴파일타임/런타임 중 언제 결정되는지.- 포인터 실패(
nullptr) vs 참조 실패(std::bad_cast)가 갈리는 이유,dynamic_cast<void*>로 최파생 주소를 얻는 용도(다중 상속에서 객체 신원 복원). - 게임 서버에서 엔티티 다운캐스트 남용의 비용 —
dynamic_cast핫패스 회피, 가상 디스패치/컴포넌트(ECS)/Visitor 로 타입 분기 설계.