Урок №171. Динамическое приведение типов. Оператор dynamic_cast

  Юрий  | 

  |

  Обновл. 15 Сен 2021  | 

 58166

 ǀ   10 

На уроке о явном преобразовании типов данных мы рассматривали использование оператора static_cast для конвертации переменных из одного типа данных в другой. На этом уроке мы рассмотрим еще один оператор явного преобразования — dynamic_cast.

Зачем нужен dynamic_cast?

Применяя полиморфизм на практике вы часто будете сталкиваться с ситуациями, когда у вас есть указатель на родительский класс, но вам нужно получить доступ к данным, которые есть только в дочернем классе. Например:

В этой программе метод getObject() всегда возвращает указатель класса Parent, но этот указатель может указывать либо на объект класса Parent, либо на объект класса Child. В случае, когда указатель указывает на объект класса Child, как мы будем вызывать Child::getName()?

Один из способов — добавить виртуальную функцию getName() в класс Parent (чтобы иметь возможность вызывать переопределение через объект класса Parent). Но, используя этот вариант, мы будем загромождать класс Parent тем, что должно быть заботой только класса Child.

Язык C++ позволяет нам неявно конвертировать указатель класса Child в указатель класса Parent (фактически, это и делает getObject()). Эта конвертация называется приведением к базовому типу (или «повышающим приведением типа»). Однако, что, если бы мы могли конвертировать указатель класса Parent обратно в указатель класса Child? Таким образом, мы могли бы напрямую вызывать Child::getName(), используя тот же указатель, и вообще не заморачиваться с виртуальными функциями.

Оператор dynamic_cast


В языке C++ оператор dynamic_cast используется именно для этого. Хотя динамическое приведение позволяет выполнять не только конвертацию указателей родительского класса в указатели дочернего класса, это является наиболее распространенным применением оператора dynamic_cast. Этот процесс называется приведением к дочернему типу (или «понижающим приведением типа»).

Использование dynamic_cast почти идентично использованию static_cast. Вот функция main() из вышеприведенного примера, где мы используем dynamic_cast для конвертации указателя класса Parent обратно в указатель класса Child:

Результат:

The name of the Child is: Banana

Невозможность конвертации через dynamic_cast

Вышеприведенный пример работает только из-за того, что указатель p на самом деле указывает на объект класса Child, поэтому конвертация успешна.

«А что произошло бы, если бы p не указывал на объект класса Child?» — спросите Вы. Это легко проверить, изменив аргумент метода getObject() из true на false. В таком случае getObject() будет возвращать указатель класса Parent на объект класса Parent. Если затем мы попытаемся использовать dynamic_cast для конвертации в Child, то потерпим неудачу, так как подобное преобразование невозможно.

Если dynamic_cast не может выполнить конвертацию, то он возвращает нулевой указатель.

Поскольку в коде, приведенном выше, мы не добавили проверку на нулевой указатель, то при выполнении ch->getName() мы попытаемся разыменовать нулевой указатель, что, в свою очередь, приведет к неопределенным результатам (или к сбою).

Чтобы сделать программу безопасной, необходимо добавить проверку результата выполнения dynamic_cast:

Правило: Всегда делайте проверку результата динамического приведения на нулевой указатель.

Обратите внимание, поскольку динамическое приведение выполняет проверку во время запуска программы (чтобы гарантировать возможность выполнения конвертации), использование оператора dynamic_cast чуть снижает производительность программы.

Также обратите внимание на случаи, в которых понижающее приведение с использованием оператора dynamic_cast не работает:

   Наследование типа private или типа protected.

   Классы, которые не объявляют или не наследуют классы с какими-либо виртуальными функциями (и, следовательно, не имеют виртуальных таблиц). В примере, приведенном выше, если бы мы удалили виртуальный деструктор класса Parent, то преобразование через dynamic_cast не выполнилось бы.

   Случаи, связанные с виртуальными базовыми классами (на сайте Microsoft вы можете посмотреть примеры таких случаев и их решения).

Понижающее приведение и оператор static_cast


Оказывается, понижающее приведение также может быть выполнено и через оператор static_cast. Основное отличие заключается в том, что static_cast не выполняет проверку во время запуска программы, чтобы убедиться в том, что вы делаете то, что имеет смысл. Это позволяет оператору static_cast быть быстрее, но опаснее оператора dynamic_cast. Если вы будете конвертировать Parent* в Child*, то операция будет «успешной», даже если указатель класса Parent не будет указывать на объект класса Child. А сюрприз вы получите тогда, когда попытаетесь получить доступ к этому указателю (который после конвертации должен быть класса Child, но, фактически, указывает на объект класса Parent).

Если вы абсолютно уверены, что операция с понижающим приведением указателя будет успешна, то использование static_cast является приемлемым. Один из способов убедиться в этом — использовать виртуальную функцию:

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

Оператор dynamic_cast и Ссылки

Хотя во всех примерах, приведенных выше, мы использовали динамическое приведение с указателями (что является наиболее распространенным), оператор dynamic_cast также может использоваться и со ссылками. Работа dynamic_cast со ссылками аналогична работе с указателями:

Поскольку в языке C++ не существует «нулевой ссылки», то dynamic_cast не может возвратить «нулевую ссылку» при сбое. Вместо этого, dynamic_cast генерирует исключение типа std::bad_cast (мы поговорим об исключениях чуть позже).

Оператор dynamic_cast vs. Оператор static_cast


Начинающие программисты путают, в каких случаях следует использовать static_cast, а в каких — dynamic_cast. Ответ довольно прост: используйте оператор dynamic_cast при понижающем приведении, а во всех остальных случаях используйте оператор static_cast. Однако, вам также следует рассматривать возможность использования виртуальных функций вместо операторов преобразования типов данных.

Понижающее приведение vs. Виртуальные функции

Есть программисты, которые считают, что dynamic_cast — это зло и моветон. Они же советуют использовать виртуальные функции вместо оператора dynamic_cast.

В общем, использование виртуальных функций должно быть предпочтительнее использования понижающего приведения. Однако в следующих случаях понижающее приведение является лучшим вариантом:

   Если вы не можете изменить родительский класс, чтобы добавить в него свою виртуальную функцию (например, если родительский класс является частью Стандартной библиотеки С++). При этом, чтобы использовать понижающее приведение, в родительском классе должны уже присутствовать виртуальные функции.

   Если вам нужен доступ к чему-либо, что есть только в дочернем классе (например, к функции доступа, которая существует только в дочернем классе).

   Если добавление виртуальной функции в родительский класс не имеет смысла. В таком случае, в качестве альтернативы, если вам не нужно создавать объект родительского класса, вы можете использовать чистую виртуальную функцию.

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (175 оценок, среднее: 4,85 из 5)
Загрузка...

Комментариев: 10

  1. Григорий:

    Всё ли, сказанное про указатели, относится и к ссылкам? Т. е, если я точно знаю, что ссылка на базовый класс указывает на производный, то могу ли я безопасно *статически* преобразовать её к ссылке на производный (класс)?

  2. Кирилл:

    Фантастические уроки. Спасибо, переводчику! Это невероятный труд, который принесёт пользу не одной тысячи людей!

    На самом деле эта тема очень сложна, спасибо за ссылку на сайт Майкрософт, там более детальнее раскрывается тема!

    1. Фото аватара Юрий:

      Пожалуйста))

  3. San_Rembak:

    "Один из способов — добавить виртуальную функцию getName() в класс Parent (чтобы иметь возможность вызывать переопределение через объект класса Parent). Но, используя этот вариант, мы будем загрязнять класс Parent тем, что должно быть заботой только класса Child.". Вы хоетли написать getObject()? А то я перечитываю и смотрю в код и не врубаюсь зачем getName() виртуалную делать?

    1. Владимир:

      ???
      Как можно сделать виртуальной функцию, не являющуюся методом класса?
      А вот "getName() виртуалную делать" в теории имеет смысле если мы хотим у указателя на Parent сразу вызвать функцию

  4. Armen:

    В уроке 56 Вы написали
    " …Основным преимуществом static_cast является проверка компилятором во время компиляции, что усложняет возможность возникновения непреднамеренных ошибок. static_cast также (специально) имеет меньшее влияние, чем C-style cast, поэтому вы не сможете случайно изменить const или сделать другие вещи, которые не намеревались делать. …"

    А в этом уроке следующее…
    " … Основное отличие заключается в том, что static_cast не выполняет проверку во время запуска программы, чтобы убедиться, что то, что вы делаете, имеет смысл. Это позволяет static_cast быть быстрее, но опаснее dynamic_cast. …"

    Есть тут несовместимост??

    1. Фото аватара Юрий:

      Проверка компилятором выполняется во время компиляции (а не запуска программы) — compile-time. Во втором примере указано, что "static_cast не выполняет проверку во время запуска программы" — т.е. во время runtime. Compile-time (во время компиляции) и runtime (когда программа запущена/работает, вплоть до её завершения) — это немного разные вещи.

      1. Армен:

        Спасибо за Ваш ответ

    2. Армен:

      Я имел ввиду, что static_cast не выполняет проверку во время запуска программы, но зато он это делает во время компиляции (Runtime после Compile-time).
      Или эти проверки разного рода характера?
      Заранее спасибо за ответ

  5. Евгений:

    Здравствуйте! У вас отличные статьи, перевод на высоте. Хотелось бы спросить, когда будут лямбды и шаблоны(Особенно интересно про вариативный шаблон)?

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

Ваш E-mail не будет опубликован. Обязательные поля помечены *