Иногда вы можете столкнуться с ситуацией, когда нужно поймать исключение, но обрабатывать его в данный момент времени не нужно (или нет возможности). Например, вы можете записать ошибку в лог-файл, а затем передать её обратно в caller для выполнения фактической обработки.
Отложенная обработка исключения
Это легко выполнимая задача при использовании кодов возврата, например:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
Database* createDatabase(std::string filename) { try { Database *d = new Database(filename); d->open(); // предположим, что выполнение этой строки приведет к генерации исключения типа int return d; } catch (int exception) { // Неудачное создание объекта Database. // Записываем ошибку в лог-файл g_log.logError("Creation of Database failed"); } return nullptr; } |
В примере, приведенном выше, функции поручено создать объект класса Database, открыть базу данных, а затем возвратить объект класса Database обратно. В таком случае, если что-то пойдет не так (например, передастся неправильный filename
), то обработчик исключений запишет ошибку в лог-файл, а затем возвратит нулевой указатель.
Теперь рассмотрим следующую функцию:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
int getIntValueFromDatabase(Database *d, std::string table, std::string key) { assert(d); try { return d->getIntValue(table, key); // генерируется исключение типа int } catch (int exception) { // Записываем ошибку в лог-файл g_log.logError("doSomethingImportant failed"); // Однако мы не обработали эту ошибку. // Что нам здесь делать? } } |
В случае успеха выполнения этой функции возвращается целочисленное значение.
Но что, если что-то пойдет не так с getIntValue()? В таком случае, функция getIntValue() сгенерирует целочисленное исключение, которое будет перехвачено блоком catch в getIntValueFromDatabase(), который запишет ошибку в лог-файл. Но как мы затем сообщим caller-у функции getIntValueFromDatabase(), что что-то пошло не так? В отличие от функции из первого примера, здесь нет хорошего кода возврата, который мы могли бы использовать (поскольку функция getIntValueFromDatabase() возвращает целочисленное значение, то любое целочисленное значение в качестве кода возврата является допустимым).
Генерация нового исключения
Одним из очевидных решений является генерация нового исключения:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
int getIntValueFromDatabase(Database *d, std::string table, std::string key) { assert(d); try { return d->getIntValue(table, key); // генерируется исключение типа int } catch (int exception) { // Записываем ошибку в лог-файл g_log.logError("doSomethingImportant failed"); throw 'q'; // генерируется исключение 'q' типа char, которое будет обрабатывать caller функции getIntValueFromDatabase() } } |
В примере, приведенном выше, программа ловит исключение типа int из getIntValue(), записывает ошибку в лог-файл, а затем выбрасывает новое исключение со значением q
типа char. Хотя генерация исключения в блоке catch может показаться странной затеей, это не запрещено. Помните, что только исключения, сгенерированные в блоке try, могут быть перехвачены блоком catch. Это означает, что исключение, сгенерированное в блоке catch, не будет перехвачено блоком catch, в котором оно находится. Вместо этого стек начнет раскручиваться, и исключение будет передано caller-у, который находится на уровне выше в стеке вызовов.
Исключение, сгенерированное в блоке catch, может быть исключением любого типа — оно не обязательно должно быть того же типа, что и исключение, которое обрабатывает блок catch.
Неправильная повторная генерация исключений
Альтернативным решением является повторная генерация одного и того же исключения:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
int getIntValueFromDatabase(Database *d, std::string table, std::string key) { assert(d); try { return d->getIntValue(table, key); // генерируется исключение типа int } catch (int exception) { // Записываем ошибку в лог-файл g_log.logError("doSomethingImportant failed"); throw exception; } } |
Хотя это работает, но здесь есть пара нюансов. Во-первых, в блоке catch не генерируется точно такое же исключение, которое обрабатывает блок catch, а генерируется копия переменной exception
. Этот вариант плох тем, что снижает производительность (незначительно, но можно было бы и без этого обойтись).
Рассмотрим, что произойдет в следующем случае:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
int getIntValueFromDatabase(Database *d, std::string table, std::string key) { assert(d); try { return d->getIntValue(table, key); // генерируется класс-исключение Child } catch (Parent &exception) { // Записываем ошибку в лог-файл g_log.logError("doSomethingImportant failed"); throw exception; // опасно: Эта строка выбрасывает в качестве исключения объект класса Parent, а не объект класса Child } } |
Здесь функция getIntValue() выбрасывает объект класса Child в качестве исключения, но блок catch принимает по ссылке объект класса Parent. Это нормально, так как мы уже узнали на уроке №162, что ссылка родительского класса может использоваться для указания на дочерний объект. Однако в таком случае копия exception
является класса Parent, а не класса Child! Другими словами, произойдет обрезка объекта класса Child()!
Мы можем это увидеть в следующей программе:
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 |
#include <iostream> class Parent { public: Parent() {} virtual void print() { std::cout << "Parent"; } }; class Child: public Parent { public: Child() {} virtual void print() { std::cout << "Child"; } }; int main() { try { try { throw Child(); } catch (Parent& p) { std::cout << "Caught Parent p, which is actually a "; p.print(); std::cout << "\n"; throw p; // обрезка объекта класса Child происходит здесь } } catch (Parent& p) { std::cout << "Caught Parent p, which is actually a "; p.print(); std::cout << "\n"; } return 0; } |
Результат выполнения программы:
Caught Parent p, which is actually a Child
Caught Parent p, which is actually a Parent
Тот факт, что вторая строка указывает на то, что Parent на самом деле является Parent, а не Child, доказывает, что произошла обрезка объекта Child.
Правильная повторная генерация исключений
К счастью, язык C++ предоставляет способ повторной генерации одного и того же исключения. Для этого нужно просто использовать ключевое слово throw внутри блока 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 33 34 35 36 37 38 39 40 41 |
#include <iostream> class Parent { public: Parent() {} virtual void print() { std::cout << "Parent"; } }; class Child: public Parent { public: Child() {} virtual void print() { std::cout << "Child"; } }; int main() { try { try { throw Child(); } catch (Parent& p) { std::cout << "Caught Parent p, which is actually a "; p.print(); std::cout << "\n"; throw; // примечание: Мы здесь повторно выбрасываем исключение } } catch (Parent& p) { std::cout << "Caught Parent p, which is actually a "; p.print(); std::cout << "\n"; } return 0; } |
Результат выполнения программы:
Caught Parent p, which is actually a Child
Caught Parent p, which is actually a Child
Ключевое слово throw в блоке catch, которое, как кажется на первый взгляд, не генерирует что-либо конкретное, на самом деле генерирует точно такое же исключение, которое было только что обработано блоком catch. Никакого копирования исключения и, следовательно, обрезки объекта не выполняется.
Таким образом, этот метод повторной генерации исключения является более предпочтительным для использования.
Правило: При повторной генерации исключения используйте ключевое слово throw без указания какого-либо идентификатора.