Dynamic Casting и RTTI
Часть раздела Объектно-ориентированное программирование путешествия по C++ на Coddy — урок 62 из 104.
Иногда при работе с полиморфизмом вам необходимо определить фактический тип объекта во время выполнения или безопасно преобразовать указатель базового класса в указатель производного класса. C++ предоставляет RTTI (Runtime Type Information) и dynamic_cast для таких ситуаций.
dynamic_cast безопасно преобразует указатели или ссылки внутри иерархии наследования. В отличие от static_cast, он выполняет проверку во время выполнения и возвращает nullptr, если преобразование недопустимо:
class Animal {
public:
virtual ~Animal() = default;
};
class Dog : public Animal {
public:
void bark() { std::cout << "Woof!" << std::endl; }
};
class Cat : public Animal {};
Animal* animal = new Dog();
Dog* dog = dynamic_cast<Dog*>(animal); // Успешно: возвращает валидный указатель
if (dog) {
dog->bark(); // Безопасно вызывать метод, специфичный для Dog
}
Cat* cat = dynamic_cast<Cat*>(animal); // Ошибка: возвращает nullptrВажно: dynamic_cast работает только с полиморфными типами (классами, имеющими хотя бы одну виртуальную функцию). Оператор typeid позволяет запрашивать фактический тип объекта:
#include <typeinfo>
Animal* pet = new Dog();
std::cout << typeid(*pet).name() << std::endl; // Выводит информацию о типе DogХотя dynamic_cast полезен, его частое использование часто указывает на проблему в проектировании. Отдавайте предпочтение виртуальным функциям, когда это возможно, так как они позволяют объекту обрабатывать специфичное для типа поведение без явной проверки типов.
Задание
ЛегкоДавайте создадим систему инспекции транспортных средств, которая использует dynamic_cast для безопасной идентификации и взаимодействия с различными типами транспортных средств. Вы создадите иерархию транспортных средств, в которой инспектору необходимо выполнять специфические для каждого типа проверки, которые поддерживают только определенные транспортные средства.
Вы распределите свой код по трем файлам:
Vehicle.h: Определите базовый классVehicle, который представляет любое транспортное средство в системе:- Защищенный член
std::string licensePlate - Конструктор, инициализирующий номерной знак
- Виртуальный метод
getDescription(), который возвращает"Vehicle: <licensePlate>" - Виртуальный деструктор
- Защищенный член
Vehicles.h: Определите три производных типа транспортных средств:Car:- Приватный член
int seatCount - Конструктор, принимающий номерной знак и количество мест
- Переопределите
getDescription(), чтобы он возвращал"Car: <licensePlate>" - Метод
inspectSeatbelts(), который выводит:Inspecting <seatCount> seatbelts in <licensePlate>
Truck:- Приватный член
double cargoCapacity(в тоннах) - Конструктор, принимающий номерной знак и грузоподъемность
- Переопределите
getDescription(), чтобы он возвращал"Truck: <licensePlate>" - Метод
inspectCargo(), который выводит:Inspecting cargo area (<cargoCapacity> tons) in <licensePlate>
Motorcycle:- Приватный член
bool hasSidecar - Конструктор, принимающий номерной знак и наличие коляски
- Переопределите
getDescription(), чтобы он возвращал"Motorcycle: <licensePlate>" - Метод
inspectHelmetStorage(), который выводит:Inspecting helmet storage in <licensePlate>, если есть коляска, илиNo helmet storage in <licensePlate>, если ее нет
- Приватный член
main.cpp: Считайте три входных значения (каждое на отдельной строке):- Номерной знак автомобиля
- Номерной знак грузовика
- Номерной знак мотоцикла
Создайте
Carс 4 местами,Truckс грузоподъемностью 10.5 тонн иMotorcycleс коляской. Сохраните все три объекта в массиве указателейVehicle*.Пройдите циклом по массиву и для каждого транспортного средства:
- Выведите его описание с помощью
getDescription() - Используйте
dynamic_castдля попытки приведения к каждому производному типу - Если приведение к
Car*прошло успешно, вызовитеinspectSeatbelts() - Если приведение к
Truck*прошло успешно, вызовитеinspectCargo() - Если приведение к
Motorcycle*прошло успешно, вызовитеinspectHelmetStorage()
Выводите пустую строку между проверками каждого транспортного средства. Очистите динамически выделенные объекты по завершении работы.
Например, при входных данных ABC-123, TRK-456 и MTR-789:
Car: ABC-123
Inspecting 4 seatbelts in ABC-123
Truck: TRK-456
Inspecting cargo area (10.5 tons) in TRK-456
Motorcycle: MTR-789
Inspecting helmet storage in MTR-789Обратите внимание, как dynamic_cast возвращает валидный указатель только тогда, когда фактический тип объекта совпадает с целевым типом. Для каждого транспортного средства только одно из трех приведений типов будет успешным, что позволит вам безопасно вызвать метод проверки, специфичный для данного типа. В этом заключается сила RTTI — определение фактического типа во время выполнения и соответствующие действия.
Шпаргалка
C++ предоставляет RTTI (Runtime Type Information — информация о типе во время выполнения) и dynamic_cast для определения фактического типа объекта во время выполнения и безопасного преобразования указателей внутри иерархии наследования.
dynamic_cast выполняет проверку во время выполнения и возвращает nullptr, если преобразование недопустимо:
class Animal {
public:
virtual ~Animal() = default;
};
class Dog : public Animal {
public:
void bark() { std::cout << "Woof!" << std::endl; }
};
Animal* animal = new Dog();
Dog* dog = dynamic_cast<Dog*>(animal); // Успешно: возвращает валидный указатель
if (dog) {
dog->bark(); // Безопасно вызывать метод, специфичный для Dog
}
Cat* cat = dynamic_cast<Cat*>(animal); // Ошибка: возвращает nullptrВажно: dynamic_cast работает только с полиморфными типами (классами, имеющими хотя бы одну виртуальную функцию).
Оператор typeid запрашивает фактический тип объекта:
#include <typeinfo>
Animal* pet = new Dog();
std::cout << typeid(*pet).name() << std::endl; // Выводит информацию о типе для DogХотя dynamic_cast и полезен, его частое использование часто указывает на проблемы в проектировании. По возможности отдавайте предпочтение виртуальным функциям.
Попробуйте сами
#include <iostream>
#include <string>
#include "Vehicle.h"
#include "Vehicles.h"
using namespace std;
int main() {
// Чтение входных данных
string carPlate, truckPlate, motorcyclePlate;
cin >> carPlate;
cin >> truckPlate;
cin >> motorcyclePlate;
// TODO: Создайте Car с 4 сиденьями
// TODO: Создайте Truck грузоподъемностью 10.5 тонн
// TODO: Создайте Motorcycle с коляской (true)
// TODO: Сохраните все три объекта в массив указателей Vehicle*
// TODO: Пройдите циклом по массиву и для каждого транспортного средства:
// 1. Выведите его описание с помощью getDescription()
// 2. Используйте dynamic_cast для попытки приведения к каждому производному типу
// 3. Если приведение к Car* прошло успешно, вызовите inspectSeatbelts()
// 4. Если приведение к Truck* прошло успешно, вызовите inspectCargo()
// 5. Если приведение к Motorcycle* прошло успешно, вызовите inspectHelmetStorage()
// 6. Выведите пустую строку между проверками каждого транспортного средства
// TODO: Очистите динамически выделенные объекты
return 0;
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП
Внешние файлыСборка и компиляция C++Заголовочные файлы и файлы исходного кодаПространства имен и область видимостиВведение в ООП на C++Классы и объектыУказатель 'this'Методы (функции-члены)Атрибуты (члены данных)Основы конструкторов и деструкторовИтоги — Простой калькулятор4Свойства классов
Члены экземпляра против статическихГеттеры и сеттерыКонстантные функции-членыКлючевое слово mutableСтатические методы и переменныеДружественные функции и классыИтоги — Менеджер банковских счетов7Наследование
Основы наследованияУровни доступа при наследованииПорядок вызова конструкторов и деструкторовПереопределение методовВиртуальные функции и VTableМножественное наследованиеВиртуальное наследованиеИтоги — Иерархия сотрудников10Обзор STL
Обзор и философия STLКонтейнеры STLИтераторыАлгоритмы STLФункторы и лямбда-выраженияИтоги: частота слов13Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonFactory и Abstract FactoryПаттерн BuilderПаттерн ObserverПаттерн Strategy2Управление памятью
Стек и кучаУказатели и ссылкиДинамическая память (new/delete)Умные указатели в C++RAII в C++Итоги — Менеджер динамического массива5Инкапсуляция
Спецификаторы доступа в C++Спецификаторы доступа: подробный разборСокрытие данныхStruct против ClassВложенные и внутренние классыИтоги — Система учета студентов8Полиморфизм
Полиморфизм компиляции и времени выполненияПерегрузка функцийВиртуальные функции: повторениеЧистые виртуальные функцииАбстрактные классыПроектирование интерфейсов на C++Dynamic Casting и RTTIИтоги: Калькулятор фигур11Продвинутые концепции ООП
Композиция против наследованияМиксины через CRTPИдиома PimplСтирание типовEnum Classes и строгая типизацияОбработка исключений в ООППользовательские иерархии исключений14Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикRAII как паттерн3Конструкторы и деструкторы
Конструктор по умолчаниюПараметризованный конструкторКонструктор копированияКонструктор перемещенияСписки инициализации конструктораДелегирующие конструкторыГлубокое погружение в деструкторыПравило трех / пяти / нуляПовторение — класс String6Перегрузка операторов
Введение в перегрузку операторовПерегрузка арифметических операторовПерегрузка операторов сравненияОператоры потоковПерегрузка оператора присваиванияПерегрузка операторов [] и ()Операторы преобразования типовИтоги — класс Matrix9Шаблоны
Шаблоны функцийШаблоны классовСпециализация шаблоновВариативные шаблоныОсновы SFINAE и Type TraitsИтоги — Обобщенный контейнер