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

  Юрий Ворон  | 

    | 

  Обновлено 2 Ноя 2018  | 

 302

 ǀ   2 

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

В этом уроке мы рассмотрим еще один оператор 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 против static_cast

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

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

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

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

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

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

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

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

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

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

  1. Евгений:

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

    1. Юрий Ворон Юрий Ворон:

      Привет. Спасибо за отзыв. Очень скоро 🙂

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

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

ВОЛШЕБНАЯ ТАБЛЕТКА ПО С++