Динамическая идентификация типов времени выполнения
Динамическая идентификация типов времени выполнения позволяет программе узнать реальный производный тип объекта, адресуемого по ссылке или по указателю на базовый класс. Реализована 2 операциями:
- dynamic_cast – поддерживает преобразование типов времени выполнения
- операция typeid идентифицирует реальный тип выражения
Операция dynamic_cast
Встроенная унарная операция dynamic_cast языка C++ позволяет безопасно трансформировать указатель на базовый класс в указатель на производный класс:
При невозможности трансформации возвращается нулевой указатель
Операция dynamic_cast (ссылки)
То же самое можно проделать и с ссылками на объект.
При невозможности ссылочного преобразования генерируется bad_cast исключение. В общем случае все операции трансформации нужно производить ожидая, что может сгенерироваться ошибка. Работу с исключительными операциями рассмотрим позднее.
Операция typeid
Встроенная унарная операция typeid позволяет установить фактический тип любых объектов
Зачем нужен dynamic_cast?
Для чего нужен dynamic_cast ? Какие преимущества имеет запись 1 по сравнению с записью 2 и какие недостатки? В каких случаях какой вариант следует использовать?
Вообще-то, в том варианте, как вы записали, не нужно приводить ничего, потому что объект производного класса уже ЯВЛЯЕТСЯ объектом базового. Так что можно просто писать
А вот если наоборот —
начинаются проблемы. Потому что обычно это говорит о плохом проектировании. Вы хотите использовать именно потомка там, где используется только предок. Т.е. по сути дописать какое-то хитрое поведение там, где о нем ничего не известно и не должно быть известно.
dynamic_cast позволяет не обрушить всю программу потому только, что вы сделали что-то неверно. Ну, например,
Здесь вы вызываете функцию-член, которая есть у Derived , но которой нет в Base . Но что будет, если вы действительно передадите в функцию указтель на Base ? Указатель на другого потомка Base , который ничего об этой функции не знает?
А dynamic_cast позволяет по крайней мере убедиться, что происходит именно то, что нужно, т.е. что там действительно указатель на Derived .
Что до приведения в стиле C — (A*)b — то тут, пожалуй, ближе всего reinterpret_cast — просто рассматривать биты как имеющие иной тип. Без каких-либо проверок. Что уже опасно, но, по крайней мере, в тексте программы в глаза будет бросаться это длинное слово — reinterpret_cast — как указатель на опасность. И это не шутка, это я повторяю Страуструпа — насчет длинных неуклюжих названий . _cast .
18.10 – Динамическое приведение типов
Еще в уроке «8.5 – Явное преобразование (приведение) типов данных и static_cast » мы изучили концепцию преобразования типов и использование static_cast для преобразования переменных из одного типа в другой.
В этом уроке мы продолжим рассмотрение другого типа приведения: dynamic_cast .
Необходимость в dynamic_cast
Имея дело с полиморфизмом, вы часто сталкиваетесь со случаями, когда у вас есть указатель на базовый класс, но вы хотите получить доступ к какой-либо информации, которая существует только в производном классе.
Рассмотрим следующую (слегка надуманную) программу:
В этой программе функция getObject() всегда возвращает указатель Base , но этот указатель может указывать либо на объект Base , либо на объект Derived . В случае, когда указатель указывает на объект Derived , как нам вызвать Derived::getName() ?
Один из способов – добавить в Base виртуальную функцию с именем getName() (чтобы мы могли вызывать ее с помощью указателя/ссылки Base и динамически разрешать ее вызов в Derived::getName() ). Но что бы эта функция вернула, если бы вы вызвали ее с помощью указателя/ссылки Base , которая на самом деле указывала на объект Base ? У него нет никакого осмысленного значения для возврата. Более того, мы загрязнили бы наш класс Base вещами, которые на самом деле должны быть заботой только класса Derived .
Мы знаем, что C++ позволяет вам неявно преобразовать указатель Derived в указатель Base (фактически, getObject() делает именно это). Этот процесс иногда называют повышающим преобразование (upcasting). Однако что, если бы существовал способ преобразовать указатель Base обратно в указатель Derived ? Тогда мы могли бы вызвать Derived::getName() напрямую, используя этот указатель, и вообще не беспокоиться о добавлении виртуальной функции.
dynamic_cast
C++ предоставляет оператор приведения с именем dynamic_cast , который можно использовать только для этой цели. Хотя динамическое приведение имеет несколько различных возможностей, наиболее распространенное использование динамического приведения – преобразование указателей базового класса в указатели производного класса. Этот процесс называется понижающим преобразованием (downcasting).
Использование dynamic_cast работает так же, как static_cast . Вот наш пример main() выше, с использованием dynamic_cast для преобразования нашего указателя Base обратно в указатель Derived :
Этот код печатает:
Сбой dynamic_cast
Приведенный выше пример работает, потому что b фактически указывает на объект Derived , поэтому преобразование b в указатель Derived проходит успешно.
Однако мы сделали довольно опасное предположение, что b указывает на объект Derived . Что, если бы b не указывал на объект Derived ? Это легко проверить, изменив аргумент getObject() с true на false . В этом случае getObject() вернет указатель Base на объект Base . Когда мы попытаемся выполнить динамическое преобразование в объект Derived , это не удастся, потому что это преобразование не может быть выполнено.
Если dynamic_cast завершается неудачей, результатом преобразования будет нулевой указатель.
Поскольку мы не проверили результат на нулевой указатель, мы обращаемся к d->getName() , что пытается разыменовать нулевой указатель, что приводит к неопределенному поведению (возможно, к сбою).
Чтобы сделать эту программу безопасной, нам нужно убедиться, что результат выполнения dynamic_cast действительно успешен:
Правило
Всегда проверяйте результат выполнения динамического приведения, проверяя результат на нулевой указатель.
Обратите внимание: поскольку dynamic_cast во время выполнения выполняет какую-то проверку согласованности (чтобы гарантировать, что преобразование может быть выполнено), использование dynamic_cast приводит к снижению производительности.
Также обратите внимание, что есть несколько случаев, когда понижающее преобразование с использованием dynamic_cast не работает:
- с защищенным или закрытым наследованием;
- для классов, которые не объявляют и не наследуют какие-либо виртуальные функции (и, следовательно, не имеют виртуальной таблицы);
- в некоторых случаях, связанных с виртуальными базовыми классами .
Понижающее преобразование с помощью static_cast
Оказывается, что понижающее преобразование также можно выполнить с помощью static_cast . Основное отличие состоит в том, что static_cast не выполняет проверку типов во время выполнения, чтобы убедиться, что то, что вы делаете, имеет смысл. Это делает использование static_cast более быстрым, но более опасным. Если вы приведете Base* к Derived* , преобразование будет «успешным», даже если указатель Base не указывает на объект Derived . Это приведет к неопределенному поведению, когда вы попытаетесь получить доступ к полученному указателю Derived (который на самом деле указывает на объект Base ).
Если вы абсолютно уверены, что указатель, для которого вы выполняете понижающее преобразование, будет корректным, использование static_cast допустимо. Один из способов убедиться, что вы знаете, на какой тип объекта вы указываете, – использовать виртуальную функцию. Вот один из способов (не самый лучший, потому что в нем используется глобальная переменная):
Но если вы готовы пройти через все эти проблемы, чтобы реализовать это (и заплатить за вызов виртуальной функции и обработку результата), вы можете просто использовать dynamic_cast .
dynamic_cast и ссылки
Хотя во всех приведенных выше примерах показано динамическое приведение указателей (что является более распространенным), dynamic_cast также может использоваться и со ссылками. Это работает аналогично тому, как dynamic_cast работает с указателями.
Поскольку C++ не имеет «нулевой ссылки», dynamic_cast не может возвращать нулевую ссылку в случае ошибки. Вместо этого, если dynamic_cast ссылки терпит неудачу, генерируется исключение типа std::bad_cast . Об исключениях мы поговорим позже в этой серии статей.
dynamic_cast против static_cast
Начинающие программисты иногда не понимают, когда использовать static_cast , а когда dynamic_cast . Ответ довольно прост: используйте static_cast , если только вы не выполняете понижающие преобразование, так как в этом случае dynamic_cast обычно лучше. Однако вам также следует подумать о том, чтобы полностью избежать приведения типов и просто использовать виртуальные функции.
Понижающее преобразование против виртуальных функций
Некоторые разработчики считают, что dynamic_cast – это зло и указывает на плохой дизайн класса. Эти программисты говорят, что вместо него вам следует использовать виртуальные функции.
В общем случае, использование виртуальной функции должно быть предпочтительнее понижающего преобразования. Однако бывают случаи, когда понижающее преобразование является лучшим выбором:
- когда вы не можете изменить базовый класс для добавления виртуальной функции (например, потому что базовый класс является частью стандартной библиотеки);
- когда вам нужен доступ к чему-то, специфичному для производного класса (например, функция доступа, которая существует только в производном классе);
- когда добавлять виртуальную функцию к базовому классу не имеет смысла (например, в базовом классе нет подходящего значения для возврата). Использование чисто виртуальной функции может быть здесь допустимо, если вам не нужно создавать экземпляры базового класса.
Предупреждение о dynamic_cast и RTTI
Динамическая идентификация типа данных (RTTI, run-time type information) – это функция C++, которая предоставляет информацию о типе данных объекта во время выполнения. Эта возможность используется в dynamic_cast . Поскольку RTTI требует значительных затрат производительности, некоторые компиляторы для оптимизации позволяют отключать RTTI. Излишне говорить, что если вы это сделаете, dynamic_cast не будет работать правильно.
dynamic_cast conversion
Safely converts pointers and references to classes up, down, and sideways along the inheritance hierarchy.
Contents
[edit] Syntax
dynamic_cast< new-type >( expression ) |
new-type | — | pointer to complete class type, reference to complete class type, or pointer to (optionally cv-qualified) void |
expression | — | lvalue (until C++11) glvalue (since C++11) of a complete class type if new-type is a reference, prvalue of a pointer to complete class type if new-type is a pointer. |
If the cast is successful, dynamic_cast returns a value of type new-type . If the cast fails and new-type is a pointer type, it returns a null pointer of that type. If the cast fails and new-type is a reference type, it throws an exception that matches a handler of type std::bad_cast .
[edit] Explanation
For the convenience of description, » expression or the result is a reference to T » means that «it is a glvalue of type T «, which follows the convention of decltype .
Only the following conversions can be done with dynamic_cast , except when such conversions would cast away constness or volatility.