Урок №179. Динамічне приведення типів. Оператор dynamic_cast

  Юрій  | 

  Оновл. 27 Вер 2021  | 

 151

На уроці про явну конвертацію типів даних ми розглядали використання оператора 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 Зірок (2 оцінок, середня: 5,00 з 5)
Loading...

Залишити відповідь

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