dynamic_cast



Изучая тему динамического полиморфизма нельзя не упомянуть про оператор приведения dynamic_cast, который создан специально для полиморфных классов.



Бывает, что в рамках работы с полиморфными классами нам необходимо выполнить приведение от указателя с одним типом к другому из этого же полиморфного семейства. Зачастую мы не можем гарантировать, что динамический тип объекта совпадает с ожидаемым. Приведение оператором static_cast сопряжено с рисками получить UB. Как же нам безопасно его выполнить?



Отличительной особенностью dynamic_cast является проверка корректности приведения во время исполнения программы. Из живого примера 1:

Device *base = new Laptop();



// Try Laptop* -> Smartphone*

// Result: `derived` is `nullptr`

auto *derived = dynamic_cast<Smartphone*>(base);

...

// Try Laptop& -> Smartphone&

// Result: throw exception std::bad_cast

auto &derived = dynamic_cast<Smartphone&>(*base);





Как мы видим, компилятор позволяет собрать программу, но, в случае попытки приведения к ложному потомку, dynamic_cast возвращает либо нулевой указатель, либо бросает исключение std::bad_cast для ссылок.



Давайте сразу договоримся о цене таких преимуществ. Как вы догадываетесь, за эдакую роскошь приходится платить тактами процессора. Это подтверждают результаты бенчмарка. Это, действительно, в несколько десятков раз медленнее, но безопаснее! Ни о каком сравнении эффективности не может быть речи, если наша программа работает неправильно.



Когда же мы можем допустить ошибку? Давайте подумаем, какие вообще могут быть сценарии приведения:

1) От наследника к предку (up cast)

2) От предка к наследнику (down cast)

3) Между ветками подсемейств полиморфных классов (cross cast, side cast)



Кейс №1 достаточно тривиален. Мы знаем иерархию наследования, текущий тип объекта и нам надо лишь вычислить смещение до полей предка. Это можно сделать даже на этапе компиляции. Тут можно применить dynamic_cast, но достаточно и static_cast. Более того, если вы примените dynamic_cast, то все равно компилятор сгенерирует инструкции, аналогичные static_cast: живой пример 2.



Кейс №2 уже сложнее тем, что динамический тип объекта неизвестен на этапе компиляции. Его можно узнать только лишь в процессе выполнения программы, прочитав виртуальный указатель. Это как раз та ситуация, когда мы должны использовать dynamic_cast, чтобы быть готовым перехватить исключение или нулевой указатель. Так же, если в иерархии классов вы используете виртуальное наследование, то static_cast неприменим, т.к. смещение неизвестно для этого кейса.



Конечно, приведение можно попытаться выполнить с помощью оператора static_cast, чтобы было побыстрее! Но чем же это грозит? Можем выстрелить себе в ногу и начать работать с полученным объектом, как с объектом другого класса. Сравним разные операторы и продемонстрируем ошибку на живом примере 3. В общем случае, мы прочитаем что-то невнятное, а если изменим данные, то ещё и испортим память, что однозначно негативно скажется на всей программе. Попытка приведения оператором static_cast к ложному потомку правомерна с точки зрения типа. Ну правда, это тип из одной иерархии, и это будет работать, если случайно динамический тип объекта включает нужного потомка. Но при работе с семейством классов, как правило, вариантов потомков больше одного. Вот будет ли это поддерживаемым кодом? Можно ли безопасно вносить изменения в иерархию классов в будущем?



Кейс №3 декомпозируется на кейсы 1 + 2: выполняем приведение к общему предку, а затем выполняем от него приведение к требуемому наследнику. Следовательно, нам так же следует использовать dynamic_cast. Вспоминаем так же об особенностях представления памяти. Прикрепляю разбор на живом примере 4.



Резюмируем. Оператор dynamic_cast имеет преимущество с точки зрения безопасности и удобства, но он работает медленнее. Тут возникает вопрос, а за что вы переплачиваете? Если вам приходится использовать dynamic_cast, то это повод подумать, насколько хорошо продумана архитектура вашего решения. Не факт, что это плохая архитектура, но это повод её пересмотреть.



#cppcore