До цього моменту ми розглядали використання винятків тільки в звичайних функціях, які не є методами класу. Проте винятки однаково корисні і в методах, і навіть в перевантаженні операторів.
Винятки в перевантаженні операторів
Розглянемо наступне перевантаження оператора індексації [] для простого цілочисельного класу-масиву:
|
1 2 3 4 |
int& ArrayInt::operator[](const int index) { return m_data[index]; } |
Хоча ця функція відмінно працює, але це тільки до тих пір, поки значенням змінної index є коректний індекс масиву. Тут явно не вистачає механізму обробки помилок. Давайте додамо стейтмент assert для перевірки index:
|
1 2 3 4 5 |
int& ArrayInt::operator[](const int index) { assert (index >= 0 && index < getLength()); return m_data[index]; } |
Тепер, якщо користувач передасть неприпустимий index, програма видасть помилку. Хоча це повідомить користувачеві, що щось пішло не так, кращим варіантом було б “по-тихому” повідомити caller, що щось пішло не так і нехай він з цим розбереться відповідним чином (як саме — ми пропишемо пізніше).
На жаль, оскільки перевантаження операторів має особливі вимоги до кількості та типу параметрів, які вони можуть приймати і повертати, немає ніякої гнучкості для передачі кодів помилок або логічних значень назад в caller. Однак, ми можемо використовувати винятки, які не змінюють сигнатуру функції, наприклад:
|
1 2 3 4 5 6 7 |
int& ArrayInt::operator[](const int index) { if (index < 0 || index >= getLength()) throw index; return m_data[index]; } |
Тепер, якщо користувач передасть неприпустимий index, operator[] згенерує виняток типу int.
Коли конструктори зазнають невдачі
Конструктори — це ще одна частина класів, де винятки можуть бути дуже корисними. Якщо конструктор не спрацював, то згенеруйте виняток, який повідомить, що об’єкт не вдалося створити. Створення об’єкту переривається, а деструктор ніколи не виконується (зверніть увагу, це означає, що ваш конструктор повинен самостійно виконувати очистку пам’яті перед генерацією винятку).
Класи-винятки
Однією з основних проблем використання фундаментальних типів даних (наприклад, типу int) в якості типів винятків є те, що вони, по своїй суті, є невизначеними. Ще серйознішою проблемою є неоднозначність того, що означає виняток, коли в блоці try є кілька стейтментів або викликів функцій:
|
1 2 3 4 5 6 7 8 9 10 |
// Використовуємо перевантаження operator[] для ArrayInt try { int *value = new int(array[index1] + array[index2]); } catch (int value) { // Які винятки ми тут ловимо? } |
У цьому прикладі, якщо ми зловимо виняток типу int, то що він нам повідомить? Чи був переданий index неприпустимим? Може оператор + викликав цілочисельне переповнення чи може оператор new не спрацював через нестачу пам’яті? Хоча ми можемо генерувати винятки типу const char*, які вказуватимуть ПРИЧИНУ збою, це все ще не дасть нам можливості обробляти винятки з різних місць по-різному.
Одним із способів вирішення цієї проблеми є використання класів-винятків. Клас-виняток — це звичайний клас, який генерується в якості винятку. Створимо простий клас-виняток, який використовуватиметься з нашим ArrayInt:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <string> class ArrayException { private: std::string m_error; public: ArrayException(std::string error) : m_error(error) { } const char* getError() { return m_error.c_str(); } }; |
Ось повна програма:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
#include <iostream> #include <string> class ArrayException { private: std::string m_error; public: ArrayException(std::string error) : m_error(error) { } const char* getError() { return m_error.c_str(); } }; class ArrayInt { private: int m_data[4]; // заради збереження простоти прикладу вкажемо значення 4 в якості довжини масиву public: ArrayInt() {} int getLength() { return 4; } int& operator[](const int index) { if (index < 0 || index >= getLength()) throw ArrayException("Invalid index"); return m_data[index]; } }; int main() { ArrayInt array; try { int value = array[7]; } catch (ArrayException &exception) { std::cerr << "An array exception occurred (" << exception.getError() << ")\n"; } } |
Використовуючи такий клас, ми можемо генерувати виняток, який повертає опис виниклої проблеми. Це дасть нам точно зрозуміти, що саме пішло не так. І, оскільки виняток ArrayException має унікальний тип, ми можемо обробляти його відповідним чином (не так як інші винятки).
Зверніть увагу, в обробниках винятків об’єкти класу-винятку приймати потрібно по посиланню, а не по значенню. Це запобіжить створенню копії винятку компілятором, що є витратною операцією (особливо, коли виняток є об’єктом класу), і запобіжить обрізці об’єктів при роботі з дочірніми класами-винятками. Передачу по адресі краще не використовувати, якщо у вас немає на це вагомих причин.
Винятки і Спадкування
Оскільки ми можемо генерувати об’єкти класів в якості винятків, а класи можуть бути отримані з інших класів, то нам потрібно враховувати, що станеться, якщо ми будемо використовувати успадковані класи в якості винятків. Виявляється, обробники можуть обробляти винятки не тільки одного певного класу, а й винятки дочірніх йому класів!
Розглянемо наступний приклад:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
#include <iostream> #include <cassert> class Parent { public: Parent() {} }; class Child: public Parent { public: Child() {} }; int main() { try { throw Child(); } catch (Parent &parent) { std::cerr << "caught Parent"; } catch (Child &child) { std::cerr << "caught Child"; } return 0; } |
Тут генерується виняток типу Child. Однак, результат виконання даної програми:
caught Parent
Що сталося?
По-перше, як ми вже говорили, дочірні класи можуть бути спіймані обробником батьківського класу. Оскільки Child є дочірнім класу Parent, то з цього випливає, що Child «є» Parent («є» — тип відносин). По-друге, коли C++ намагається знайти обробник для згенерованого винятку, він робить це послідовно. Перше, що він перевіряє — чи підходить обробник винятків класу Parent для винятків класу Child. Оскільки Child «є» Parent, то блок catch для об’єктів класу Parent підходить і, відповідно, виконується! У цьому випадку блок catch для об’єктів класу Child ніколи не виконається.
Щоб цей приклад працював по-іншому, нам потрібно змінити порядок послідовності блоків catch:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
#include <iostream> #include <cassert> class Parent { public: Parent() {} }; class Child: public Parent { public: Child() {} }; int main() { try { throw Child(); } catch (Child &child) { std::cerr << "caught Child"; } catch (Parent &parent) { std::cerr << "caught Parent"; } return 0; } |
Результат:
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+) дочірні йому класи-винятки!
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <iostream> #include <exception> // для std::exception #include <string> // для цього прикладу int main() { try { // Тут повинен знаходитися код, який використовує Стандартну бібліотеку С++. // Зараз ми навмисно спровокуємо генерацію одного з винятків std::string s; s.resize(-1); // генерується виняток std::bad_alloc } // Цей обробник ловить std::exception і всі дочірні йому класи-винятки catch (std::exception &exception) { std::cerr << "Standard exception: " << exception.what() << '\n'; } return 0; } |
Результат виконання програми:
Standard exception: string too long
У цьому прикладі все досить просто. В std::exception є віртуальний метод what(), який повертає рядок C-style з описом винятку. Більшість дочірніх класів перевизначають функцію what(), змінюючи це повідомлення. Зверніть увагу, даний рядок C-style призначений для використання тільки в якості опису.
Іноді нам потрібно буде обробляти певний тип винятків трохи інакше, ніж інші типи винятків. У такому випадку ми можемо додати обробник винятків для цього конкретного типу, а всі інші винятки «перенаправити» в батьківський обробник. Наприклад:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
try { // Тут повинен знаходитися код, який використовує Стандартну бібліотеку С++ } // Цей обробник ловить std::bad_alloc і всі дочірні йому класи-винятки catch (std::bad_alloc &exception) { std::cerr << "You ran out of memory!" << '\n'; } // Цей обробник ловить std::exception і всі дочірні йому класи-винятки catch (std::exception &exception) { std::cerr << "Standard exception: " << exception.what() << '\n'; } |
У цьому прикладі винятки типу std::bad_alloc перехоплюються і обробляються першим обробником. Винятки типу std::exception і всіх інших дочірніх йому класів-винятків обробляються другим обробником.
Такі ієрархії спадкування дозволяють використовувати певні обробники для перехоплення певного типу винятків або для перехоплення одним (батьківським) обробником всієї ієрархії винятків.
Використання стандартних винятків напряму
Ніщо не генерує std::exception напряму, і ви також повинні дотримуватися цього правила. Однак, ви можете генерувати винятки інших класів зі Стандартної бібліотеки С++, якщо вони адекватно відображають ваші потреби. Знайти список всіх стандартних класів-винятків зі Стандартної бібліотеки С++ ви можете тут.
std::runtime_error (знаходиться в заголовку stdexcept) є популярним вибором, тому що має загальне ім’я, а конструктор приймає налаштовуване повідомлення:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <iostream> #include <stdexcept> int main() { try { throw std::runtime_error("Bad things happened"); } // Цей обробник ловить std::exception і всі дочірні йому класи-винятки catch (std::exception &exception) { std::cerr << "Standard exception: " << exception.what() << '\n'; } return 0; } |
Результат:
Standard exception: Bad things happened
Створення власних класів-винятків, дочірніх класу std::exception
Звичайно, ви можете створити свої власні класи-винятки, дочірні класу std::exception, і перевизначити віртуальний константний метод what(). Ось вищенаведена програма, але вже з класом-винятком ArrayException, дочірнім std::exception:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
#include <iostream> #include <string> #include <exception> // для std::exception class ArrayException: public std::exception { private: std::string m_error; public: ArrayException(std::string error) : m_error(error) { } // Повертаємо std::string в якості константного рядка C-style // const char* what() const { return m_error.c_str(); } // до C++11 const char* what() const noexcept { return m_error.c_str(); } // C++11 і новіші версії }; class ArrayInt { private: int m_data[4]; // щоб не ускладнювати, вкажемо значення 4 в якості довжини масиву public: ArrayInt() {} int getLength() { return 4; } int& operator[](const int index) { if (index < 0 || index >= getLength()) throw ArrayException("Invalid index"); return m_data[index]; } }; int main() { ArrayInt array; try { int value = array[7]; } catch (ArrayException &exception) // спочатку ловимо винятки дочірнього класу-винятку { std::cerr << "An array exception occurred (" << exception.what() << ")\n"; } catch (std::exception &exception) { std::cerr << "Some other std::exception occurred (" << exception.what() << ")\n"; } } |
У C++11 до віртуальної функції what() додали специфікатор noexcept (який означає, що функція обіцяє не генерувати винятки самостійно). Отже, в C++11 і в новіших версіях наше перевизначення методу what() також повинно мати специфікатор noexcept.
Вам вирішувати, чи хочете ви створювати свої власні класи-винятки, використовувати класи-винятки зі Стандартної бібліотеки С++ чи писати класи-винятки, дочірні std::exception. Все залежить від ваших цілей.
