Явное приведение dynamic_cast в С++, начало знакомства

dynamic_cast — это функция, с помощью которой можно преобразовывать тип во время выполнения программы. Может применяться только к указателям или ссылкам.
  • dynamic_cast — наиболее используемый компонент RTTI (RTTI работает только для классов, имеющих виртуальные функции.)
RTTI — это механизм динамической идентификация типов (Run Time Type Identification — RTTI). Это обозначает, что типы объектов можно опознавать уже после компиляции и запуска программы. Многими устаревшими реализациями компиляторов С++ механизм RTTI не поддерживается. В некоторых компиляторах имеются опции, позволяющие включать или отключать RTTI.
Если есть базовый класс и произведённые от него наследники, то указатель, указывающий на объект базового класса, может быть направлен на любого наследника без проблем.

У производного класса может быть определён некоторый метод. Определённый в наследнике метод может быть или частью механизма полиморфизма или просто собственным методом относительно своего класса. Если этот метод задействуется путём использования механизма полиморфизма, то наследник определяется автоматически (см. тему Виртуальные функции. Поверхностное знакомство):


Несмотря на то, что указательная переменная объявлена как указатель, направляющийся на объекты класса Base, правильный тип объекта опознаётся без проблем благодаря тому что используемая сейчас функция в класса-родителе (в классе Base) объявлена как виртуальная. Задействован механизм полиморфизма. Но бывает так, что нужно описать свою собственную для некоторого наследника функцию, которую делать в родителе идеологически будет неправильно. В таком случае нужный тип автоматически определиться не может:


Листинг #3 не компилируется потому что указатель объявлен как указатель, должный указывать на объекты типа Base. Внутри класса Base не существует виртуальной функции some_function(). Правильный тип под нужды указательной переменной автоматически не определяется: как был тип A, так и остался типом A. Происходит зазыв несуществующей функции вообще. Чтобы произвести вызов функции some_function(), нужно объяснить компилятору, функцию какого класса мы хотим использовать. Делается такое путём явного преобразования типов. Поначалу может быть неважно, какой механизм преобразований задействовать: static_cast, reinterpret_cast, другой… Но может получиться так, что нужно будет выяснять тип и вот в таком случае либо мы пишем свою собственную опознавалку классов (см. пример из статьи С++. Понижающее и повышающее преобразование типов) или опознаём классы без написания своих костылей. Чтобы не вставлять свои костыли в код, можно проверять типы уже во время выполнения программы путём использования функции dynamic_cast.
  • Функция dynamic_cast — наиболее часто используемый компонент RTTI. Она не отвечает, на какой тип объекта указывает указатель. Вместо этого она дает ответ на вопрос, можно ли безопасно присвоить адрес объекта указателю на некоторый тип.
В качестве примера будет реализована простая программа, релизующая поведение самой обычной рыбы.


В листинге #4 написан основной класс, который представляет собой обобщение: Рыба, от которого произведено два класса-наследника, представляющих собой определённый вид рыб: Тунец и Карп. Чтобы в ходе разбора программы было немного легче ориентироваться, к названиям, представляющим собой классы, приставлен символ T, символизирующий, что такое-то название — это обозначение типа. Кроме классов описана функция, внутри которой возникает необходимость опознавания необходимого типа. Например, если мы создадим меню, то указатель, который выступает в качестве параметра функции, не поспособствует правильному определению типа, если зазываемая функция объекта не полиморфна.
Мы можем внутри функции использовать преобразования не dynamic_cast, но мы не можем узнать пришедший в функцию тип без дополнительных телодвижений в таком случае. Чтоб было понятнее, немного изменим программу, добавив возможность выбора пользователю:


Листинг #5 демонстрирует возникновение ситуации, где зарождается необходимость распознования типов. Ведь чтобы вызвать функцию именно того объекта, который должен обрабатываться внутри функции, нужно каким-то образом узнать, объект какого именно класса в функции должен быть обработан. Поскольку функции, которые планировалось использовать, оказались неполиморфными, то использовать их стало затруднительно: даже если мы можем преобразовать тип к какому-нибудь подходящему, мы не можем знать, какой выбор сделан пользователем. Исходя из того, что мы не можем знать, какой выбар сделан, мы не можем знать, а какую же в нужный момент функцию нужно будет зазывать.
Решить подобную проблему малой кровью можно путём задействования dynamic_cast. Мы задаёмся вопросом: "А можем ли мы преобразовать тип объекта к таком-то типу?" — и если ответ положительный, то мы можем использовать функцию подходящего типа, т. е. нужного класса, класса, к которому пробуем свести обобщение.


  • dynamic_cast всегда возвращает или результат успешного приведения, или результат неудачи.
  • Если dynamic_cast использовался для приведения указателя, то в случае неудачи возвращается нулевой адрес.
  • Если dynamic_cast использовался для приведения ссылки, то в случае неудачи возвращается исключение bad_cast.
Благодаря тому что dynamic_cast даёт нам знать о неудаче или об успехе путём возвращаемого значения, мы можем и должны использовать проверку на успех или неудачу всегда при применении dynamic_cast. Различие между возвращаемыми значениями относительно используемости ссылки или указателя существует потому что ссылки, в отличие от указателя, не могут указывать вникуда, они всегда должны быть связаны с каким-то существующим объектом по правилам языка С++. В листинге #6 используются указатели, поэтому проверка if происходит на факт указывания указателем в нулевой адрес. Если указатель указывает на нулевой адрес, то ветка if не срабатывает, иначе — срабатывает. Вы можете убрать проверки из листинга #6 и сможете увидеть, что сработают ненужные нам по идее действия.
Когда мы производим преобразование от наиболее старших классов к младшим классам, то мы делаем нисходящее преобразование, когда от младших классов к старшим, тогда делаем восходящее преобразование. Более подробно в теме С++. Понижающее и повышающее преобразование типов
В показанном коде внутри функции foo() происходило нисходящее преобразование. такие преобразования потенциально опасны.

  • Единственным гарантированно безопасным приведением является такое, в котором указатель имеет тот же тип, что и объект, либо базовый для этого объекта тип (непосредственный или более дальний предок).
В написанном ниже имеется в виду указатель, в который присваивается адрес:

— если у указателя тип такой же, как у объекта, который даёт ориентир на приведение, — то значит, типы равны, а коли типы равны, то и приведение безопасно.
— указатель на основной тип можно безопасно использовать как указатель на подтип.

Основной материал, описывающий знакомство с dynamic_cast, на этот момент уже написан. Сейчас мы немного отвлечёмся и посмотрим на безопасные и небезопасные приведения типов, при этом используем приведение в стиле языка С.

Приведение #1 — безопасно. pm имеет тип Grand и сводится к типу Magnificient. Несмотря на то, что указатель pm имеет тип Grand, направлен он на объект типа Magnificient. Чтобы выравнять тип, мы производим нисходящее преобразование и получаем из преобразования адрес объекта. Преобразованием мы вывели адрес объекта, где тип объекта Magnificient. В конечном счёте мы присваиваем в указатель, предназначенный для хранения адресов расположений объектов типа Magnificient адрес существуещего объекта, имеющего тип Magnificient. Это безопасно.
Приведение #2 — небезопасно. pg имеет тип Grand и сводится к типу Magnificient. Указатель pg имеет тип Grand и направлен он на объект типа Grand. Можно сказать, что тип выравнен, но мы искривляем его путём выравнивания понижением к Magnificient. Преобразованием мы вывели адрес объекта, где тип объекта Magnificient. Проблема в уже произошедшем искривлении. В конечном счёте мы присваиваем в указатель, предназначенный для хранения адресов расположений объектов типа Magnificient, адрес существующего объекта, имеющего тип Magnificient. Это небезопасно из-за искривляюще-понижающего преобразования. Нисходящее преобразование потенциоально опасно.
Приведение #3 — безопасно. pm имеет тип Grand и сводится к типу Magnificient. Несмотря на то, что указатель pm имеет тип Grand, направлен он на объект типа Magnificient. Чтобы выравнять тип, мы производим нисходящее преобразование и получаем из преобразования адрес объекта. В конечном итоге мы присваиваем в указатель p3 адрес, на котором расположился объект типа Magnificient. Поскольку p3 предназначен для хранения адресов, тип которых ступенькой выше, то происходит неявное преобразование, выравнивающее типы. Тип Magnificient повышается до Superb. Это безопасно. Преобразования снизу-вверх всегда безопасны.
  • Вопрос о безопасности приведения типа имеет больший приоритет, чем вопрос о конечном типе объекта, к которому ведёт указатель.
Ниже приводится код, написанный в книге Прата. В том коде имеется функция GetOne, внутри которой создаётся указатель на объект случайно выбранного типа, возвращаемый из функции как указатель, указывающий на тип Grand. Внутри функции main написан цикл, в котором в указатель pg, объект-цель которого как раз Grand, присваивается адрес, отдаваемый функцией GetOne. После того, как подобрался адрес объекта случайного типа, используется метод Say, который будет выполняться для нужного объекта. Метод Say присутствует в двух классах из трёх, в первородном классе этого метода нет, поэтому для первого класса вызываться метод Say в примере не будет, это достигается благодаря тому, что ипользуется dynamic_cast и его возможность проверки на успешность приведения. Писать можно много, но лучше проанализируйте код.

Использованные материалы:

Язык программирования C++. Лекции и упражнения. 6-е изд (Стивен Прата)
Освой самостоятельно C++ за 21 день, 7-е издание (Сиддхартха Рао)

Все комментарии на сайте проверяются, поэтому ваш комментарий может появиться не сразу. Для вставки кода в комментарий используйте теги: [php]ВАШ_КОД[/php]

Добавить комментарий

Ваш e-mail не будет опубликован.

58 + = 59

Поиск

 
     

Случайная книга в электронном формате

https://www.litres.ru/nikolay-sekunov/programmirovanie-na-c-v-linux/?lfrom=15589587
Яндекс.Метрика
НАГРАДИ АВТОРА САЙТА
WEBMONEY
R375024497470
U251140483387
Z301246203264
E149319127674

Программист на ночь ставит два стакана. Один - с водой, другой - без воды. Первый - на случай, если захочет пить, Второй - если не захочет.

Выражаю свою признательность

  • Максиму очень признателен за указание на мои ошибки и неточности.
  • Sergio ===> за оказание помощи в исправлении моих ошибок
  • Gen ===> за правильное стремление помочь другим новичкам и выявления моих ошибок