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

  Юрий  | 

    | 

  Обновл. 27 Июн 2019  | 

 4891

 ǀ   6 

В уроке о явном преобразовании типов данных мы рассматривали использование оператора static_cast для конвертации переменных из одного типа данных в другой. В этом уроке мы рассмотрим ещё один из операторов casts: 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 (31 оценок, среднее: 4,74 из 5)
Загрузка...

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

  1. Аватар San_Rembak:

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

  2. Аватар 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).
      Или эти проверки разного рода характера?
      Заранее спасибо за ответ

  3. Евгений Евгений:

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

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

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