Урок №193. Класи-винятки і Спадкування

  Юрій  | 

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

 31

До цього моменту ми розглядали використання винятків тільки в звичайних функціях, які не є методами класу. Проте винятки однаково корисні і в методах, і навіть в перевантаженні операторів.

Винятки в перевантаженні операторів

Розглянемо наступне перевантаження оператора індексації [] для простого цілочисельного класу-масиву:

Хоча ця функція відмінно працює, але це тільки до тих пір, поки значенням змінної index є коректний індекс масиву. Тут явно не вистачає механізму обробки помилок. Давайте додамо стейтмент assert для перевірки index:

Тепер, якщо користувач передасть неприпустимий index, програма видасть помилку. Хоча це повідомить користувачеві, що щось пішло не так, кращим варіантом було б “по-тихому” повідомити caller, що щось пішло не так і нехай він з цим розбереться відповідним чином (як саме — ми пропишемо пізніше).

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

Тепер, якщо користувач передасть неприпустимий index, operator[] згенерує виняток типу int.

Коли конструктори зазнають невдачі

Конструктори — це ще одна частина класів, в якій винятки можуть бути дуже корисними. Якщо конструктор не спрацював, то згенеруйте виняток, який повідомить, що об’єкт не вдалося створити. Створення об’єкту переривається, а деструктор ніколи не виконується (зверніть увагу, це означає, що ваш конструктор повинен самостійно виконувати очистку пам’яті перед генерацією винятку).

Класи-винятки

Однією з основних проблем використання фундаментальних типів даних (наприклад, типу int) в якості типів винятків є те, що вони, по своїй суті, є невизначеними. Ще більш серйозною проблемою є неоднозначність того, що означає виняток, коли в блоці try є кілька стейтментів або викликів функцій:

У цьому прикладі, якщо ми зловимо виняток типу int, то що він нам повідомить? Чи був переданий index неприпустимим? Може оператор + викликав цілочисельне переповнення чи може оператор new не спрацював через нестачу пам’яті? Хоча ми можемо генерувати винятки типу const char*, які вказуватимуть ПРИЧИНУ збою, це все ще не дасть нам можливості обробляти винятки з різних джерел по-різному.

Одним із способів вирішення цієї проблеми є використання класів-винятків. Клас-виняток — це звичайний клас, який викидається в якості винятку. Створимо простий клас-виняток, який використовуватиметься з нашим ArrayInt:

Ось повна програма:

Використовуючи такий клас, ми можемо генерувати виняток, який повертає опис виниклої проблеми, це дасть нам точно зрозуміти, що саме пішло не так. І, оскільки виняток ArrayException має унікальний тип, ми можемо обробляти його відповідним чином (не так як інші винятки).

Зверніть увагу, в обробниках винятків об’єкти класу-винятку приймати потрібно по посиланню, а не по значенню. Це запобіжить створенню копії винятку компілятором, що є витратною операцією (особливо, коли виняток є об’єктом класу), і запобіжить обрізці об’єктів при роботі з дочірніми класами-винятками. Передачу по адресу краще не використовувати, якщо у вас немає на це вагомих причин.

Винятки і Спадкування

Так як ми можемо викидати об’єкти класів в якості винятків, а класи можуть бути отримані з інших класів, то нам потрібно враховувати, що станеться, якщо ми будемо використовувати успадковані класи в якості винятків. Виявляється, обробники можуть обробляти винятки не тільки одного певного класу, а й винятки дочірніх йому класів!

Розглянемо наступний приклад:

Тут викидається виняток типу Child. Однак, результат виконання даної програми:

caught Parent

Що сталося?

По-перше, як ми вже говорили, дочірні класи можуть бути спіймані обробником батьківського класу. Оскільки Child є дочірнім класу Parent, то з цього випливає, що Child «є» Parent («є» — тип відносин). По-друге, коли C++ намагається знайти обробник для викинутого винятку, він робить це послідовно. Перше, що він перевіряє — чи підходить обробник винятків класу Parent для винятків класу Child. Оскільки Child «є» Parent, то блок catch для об’єктів класу Parent підходить і, відповідно, виконується! У цьому випадку блок catch для об’єктів класу Child ніколи не виконається.

Щоб цей приклад працював по-іншому, нам потрібно змінити порядок послідовності блоків catch:

Результат:

caught Child

Таким чином, обробник Child ловитиме і оброблятиме винятки класу Child. Винятки класу Parent не відповідають обробнику Child (Child «є» Parent, але Parent «не є» Child) і, відповідно, будуть оброблятися тільки обробником Parent.

Правило: Обробники винятків дочірніх класів повинні знаходитися перед обробниками винятків батьківського класу.

Інтерфейсний клас std::exception

Багато класів та операторів зі Стандартної бібліотеки С++ генерують класи-винятки при збої. Наприклад, оператор new і std::string можуть генерувати std::bad_alloc при нестачі пам’яті. Невдале динамічне приведення типів за допомогою оператора dynamic_cast генерує виняток std::bad_cast тощо. Починаючи з C++14, існує більше 20 класів-винятків, які можуть бути згенеровані, а в C++17 їх ще більше.

Доброю новиною є те, що всі ці класи-винятки є дочірніми класу std::exception. std::exception — це невеликий інтерфейсний клас, який використовується в якості батьківського класу для будь-якого винятку, який генерується в Стандартній бібліотеці C++.

У більшості випадків, якщо виняток генерується Стандартною бібліотекою С++, то нам все рівно, чи було це невдале виділення, конвертування чи що-небудь інше. Нам досить знати, що сталося щось катастрофічне, через що в нашій програмі стався збій. Завдяки std::exception ми можемо налаштувати обробник винятків типу std::exception, який ловитиме і оброблятиме як std::exception, так і всі (20+) дочірні йому класи-винятки!

Результат виконання програми:

Standard exception: string too long

У цьому прикладі все досить просто. В std::exception є віртуальний метод what(), який повертає рядок C-style з описом винятку. Більшість дочірніх класів перевизначають функцію what(), змінюючи це повідомлення. Зверніть увагу, даний рядок C-style призначений для використання тільки в якості опису.

Іноді нам потрібно буде обробляти певний тип винятків трохи інакше, ніж інші типи винятків. У такому випадку ми можемо додати обробник винятків для цього конкретного типу, а всі інші винятки «перенаправити» в батьківський обробник. Наприклад:

У цьому прикладі винятки типу std::bad_alloc перехоплюються і обробляються першим обробником. Винятки типу std::exception і всіх інших дочірніх йому класів-винятків обробляються другим обробником.

Такі ієрархії спадкування дозволяють використовувати певні обробники для перехоплення певного типу винятків або для перехоплення одним (батьківським) обробником всієї ієрархії винятків.

Використання стандартних винятків напряму

Ніщо не генерує std::exception напряму, і ви також повинні дотримуватися цього правила. Однак, ви можете генерувати винятки інших класів зі Стандартної бібліотеки С++, якщо вони адекватно відображають ваші потреби. Знайти список всіх стандартних класів-винятків зі Стандартної бібліотеки С++ ви можете тут.

std::runtime_error (знаходиться в заголовку stdexcept) є популярним вибором, тому що має загальне ім’я, а конструктор приймає налаштовуване повідомлення:

Результат:

Standard exception: Bad things happened

Створення власних класів-винятків, дочірніх класу std::exception

Звичайно, ви можете створити свої власні класи-винятки, дочірні класу std::exception, і перевизначити віртуальний константний метод what(). Ось вищенаведена програма, але вже з класом-винятком ArrayException, дочірнім std::exception:

У C++11 до віртуальної функції what() додали специфікатор noexcept (який означає, що функція обіцяє не генерувати винятки самостійно). Отже, в C++11 і в новіших версіях наше перевизначення методу what() також повинно мати специфікатор noexcept.

Вам вирішувати, чи хочете ви створювати свої власні класи-винятки, використовувати класи-винятки зі Стандартної бібліотеки С++ чи писати класи-винятки, дочірні std::exception. Все залежить від ваших цілей.

Оцінити статтю:

1 Зірка2 Зірки3 Зірки4 Зірки5 Зірок (Немає Оцінок)
Loading...

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

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