RTTI в C++ — typeid и dynamic_cast
RTTI (Run-Time Type Information) — способ узнать реальный тип объекта уже после запуска программы и безопасно привести указатель или ссылку в иерархии классов.
Нужен реже, чем кажется новичку: чаще достаточно виртуальных методов.
Но для отладки, плагинов и редкой навигации по дереву типов RTTI незаменим.
Статический и динамический тип
Когда вы пишете:
Base* ptr = new Derived();
у переменной ptr два "типа":
| Тип | Когда известен | В примере |
|---|---|---|
| Статический | при компиляции | Base* — компилятор считает, что указатель на Base |
| Динамический | в момент выполнения | объект в памяти — Derived |
Обычные вызовы через ptr идут по правилам статического типа, кроме виртуальных функций — там вызывается метод динамического типа. RTTI отвечает на вопрос: "А кто на самом деле лежит за этим Base*?"
Полиморфизм и vtable: Объектно-ориентированное программирование в C++. Оператор typeid в контексте операторов: Операторы и выражения в C++.
Что такое RTTI
RTTI — механизм C++, встроенный в полиморфные классы (с хотя бы одной virtual функцией, обычно виртуальным деструктором).
Основные инструменты:
| Инструмент | Заголовок | Назначение |
|---|---|---|
typeid | <typeinfo> | имя типа, сравнение type_info |
dynamic_cast | встроенный | приведение с проверкой иерархии |
Без виртуальных функций класс неполиморфный — typeid и dynamic_cast для иерархии такого класса не дадут динамической информации о подтипе.
Когда нужен RTTI
Пример — UI из Widget, TextBox, Image. Пока рисуете через виртуальный интерфейс, RTTI не нужен.
RTTI уместен точечно:
- логирование типа при сбое;
- миграция legacy без аккуратного
virtualконтракта; - плагины с типами из внешнего модуля.
Это дополнение к полиморфизму, а не замена virtual.
typeid
Код ITЗагрузка примера кода…
Разбор:
virtual ~Base()делаетBaseполиморфным типом, поэтомуtypeid(obj)может вернуть динамический тип.typeid(obj)вprint_typeанализирует фактический объект по ссылке, а не только тип параметраBase.std::type_infoсодержит служебную информацию о типе;name()обычно используют для диагностики и логов.- Сравнение
typeid(d) == typeid(Derived)показывает строгую проверку соответствия типа во время выполнения. - Код демонстрирует разницу между статическим типом переменной и реальным типом объекта в памяти.
Разбор выражения typeid(obj)
objимеет типconst Base&, но объект в памяти —Derived.- Класс полиморфный (есть
virtual ~Base()). typeid(obj)возвращает ссылку наstd::type_infoдля динамического типа —Derived.ti.name()— строка с именем типа; у GCC/Clang часто mangled (внутреннее имя компилятора), у MSVC может быть читабельнее.
Когда тип статический
- Для неполиморфных типов
typeid(T)иtypeid(expr)дают статический типexpr. - Для указателя без разыменования
typeid(ptr)— тип указателя (Base*), а не объекта.
std::type_info сравнивают через == и !=. Копировать и присваивать type_info нельзя — это синглтоноподобные объекты рантайма.
Где уместен typeid: логирование, отладка, фабрики "по имени типа". Для ветвления бизнес-логики чаще лучше виртуальный метод, enum class Kind или std::variant на закрытом наборе типов.
dynamic_cast
Приведение типа — "считать указатель/ссылку другого типа в иерархии". static_cast доверяет программисту; dynamic_cast проверяет в рантайме, возможно ли такое приведение.
Указатели — при неудаче — nullptr
Base* base = new Derived();
if (auto* derived = dynamic_cast<Derived*>(base)) {
derived->specific_method();
}
delete base;
Разбор:
dynamic_cast<Derived*>(base)выполняет runtime-проверку, можно ли безопасно привестиBase*кDerived*.- При успешном приведении возвращается валидный указатель
derived, иначе результат —nullptr. - Конструкция
if (auto* derived = ...)ограничивает область видимости переменной блокомif. - Вызов
specific_method()безопасен только внутри ветки успешного приведения. delete base;требует виртуального деструктора в базе, иначе уничтожение будет некорректным.
Разбор:
| Часть | Смысл |
|---|---|
dynamic_cast<Derived*>(base) | "Если base реально указывает на Derived (или наследника Derived), верни Derived*; иначе nullptr" |
if (auto* derived = ...) | C++17: объявление переменной прямо в условии |
Вызов specific_method() | Безопасен только внутри ветки, где приведение успешно |
Ссылки — при неудаче — исключение
void use(Base& b) {
try {
Derived& d = dynamic_cast<Derived&>(b);
d.specific_method();
} catch (const std::bad_cast&) {
// b ссылался на объект, который не является Derived
}
}
Разбор:
- Для ссылок
dynamic_cast<Derived&>(b)не имеет вариантаnullptr, поэтому при неудаче выбрасываетstd::bad_cast. try/catchоформляет безопасный путь, когда подтип заранее неизвестен.- Такой стиль полезен в адаптерах и легаси-коде, где нужно мягко обработать "не тот тип".
Derived& dпосле успешного приведения даёт полный доступ к API наследника без дополнительных проверок.- Логика хорошо сочетается с диагностикой через
typeid(b).name()вcatch-ветке.
Для ссылок нет значения "нулевая ссылка", поэтому ошибка — std::bad_cast (Иерархия исключений в стандартной библиотеке C++).
Когда dynamic_cast уместен
| Уместно | Лучше другой приём |
|---|---|
| редкая навигация (UI-элементы, узлы сцены) | длинные цепочки if (dynamic_cast<A*>...) else if (dynamic_cast<B*>...) |
| плагины с общим базовым интерфейсом | дублировать то же через virtual int kind() без RTTI |
| клонирование / сериализация по типу | "зоопарк" типов вместо Visitor |
Предпочтение в прикладном коде — virtual метод clone(), accept(Visitor&), закрытый enum class + switch.
Диагностика ошибок приведения
На практике полезно сразу добавлять диагностический вывод:
void handle(Base& b) {
if (auto* d = dynamic_cast<Derived*>(&b)) {
d->specific_method();
return;
}
std::cerr << "Unexpected type: " << typeid(b).name() << '\n';
}
Разбор:
- Сначала пробуется безопасное приведение к ожидаемому подтипу
Derived. - Если приведение прошло, вызывается специализированная ветка обработки и выполняется
return. - Если не прошло,
typeid(b).name()пишет фактический тип в лог, ускоряя разбор инцидента. - Такой шаблон уменьшает "тихие" ошибки, когда объект пропускается без явной причины.
- Подход особенно удобен на границе модулей и при интеграции плагинов.
Такой приём сокращает время поиска дефектов в больших системах, особенно при поддержке легаси-кода.
Сравнение с static_cast
Derived* p = static_cast<Derived*>(base_ptr);
Разбор:
static_castвыполняется без проверки динамического типа объекта во время выполнения.- Если
base_ptrне указывает наDerived, дальнейшее использованиеpведёт к UB. - Этот вариант допустим только когда корректность приведения гарантирована архитектурой.
- Для неизвестного подтипа безопаснее
dynamic_castс проверкой результата. - Фрагмент подчёркивает компромисс: скорость
static_castпротив безопасностиdynamic_cast.
static_cast не проверяет иерархию в runtime. Если base_ptr указывал на другой подтип, вызов методов Derived даёт неопределённое поведение (часто падение).
static_cast | dynamic_cast | |
|---|---|---|
| Проверка в runtime | нет | да (для полиморфных типов) |
| Скорость | быстрее | дороже |
| Безопасность при неизвестном подтипе | риск UB | nullptr или bad_cast |
Другие приведения: Операторы и выражения в C++, чек-лист C++ — чек-лист.
Стоимость и отключение RTTI
Компилятор хранит таблицы типов для полиморфных классов (рядом с vtable). Флаги отключения:
- GCC/Clang:
-fno-rtti - MSVC:
/GR-
В embedded и игровых движках RTTI иногда отключают ради размера бинарника. Тогда typeid и dynamic_cast недоступны. Обзор "pay for what you use": C++ — углублённые темы.
Как строить код в проектах без RTTI
Когда RTTI выключен флагом сборки, команды обычно переходят на один из рабочих шаблонов:
- виртуальный метод
kind()сenum class; - Visitor для закрытого набора подтипов;
- явная регистрация обработчиков в фабрике.
Главная идея — держать проверку типа в одном архитектурном месте, вместо разрозненных проверок по коду.
Мини-шпаргалка
Полиморфный класс (virtual) ──► можно typeid / dynamic_cast
Нужна ветка логики по типу ──► сначала virtual / Visitor / variant
Нужно привести указатель ──► dynamic_cast + проверка на nullptr
Нужно привести ссылку ──► dynamic_cast + catch bad_cast
Разбор:
- Шпаргалка фиксирует порядок принятия решений: сначала полиморфный дизайн, потом RTTI-инструменты.
- Первая строка напоминает, что RTTI корректно работает только с полиморфными базовыми типами.
- Блок про
virtual / Visitor / variantснижает риск разрастанияif dynamic_cast. - Для указателей указан безопасный путь через
nullptr-проверку. - Для ссылок отдельно отмечена необходимость обработки
std::bad_cast.
Связанные материалы
- ООП в C++ — vtable, virtual, abstract
- Обработка исключений — try/catch при
bad_cast - Системное программирование — проекты без RTTI
- Композиция и наследование — где RTTI действительно нужен редко
- Класс в C++ —
this,static,friend, вложенные типы