Перейти к основному содержимому

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)

  1. obj имеет тип const Base&, но объект в памяти — Derived.
  2. Класс полиморфный (есть virtual ~Base()).
  3. typeid(obj) возвращает ссылку на std::type_info для динамического типа — Derived.
  4. 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_castdynamic_cast
Проверка в runtimeнетда (для полиморфных типов)
Скоростьбыстреедороже
Безопасность при неизвестном подтипериск UBnullptr или 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.

Связанные материалы