20.6 – Повторное выбрасывание исключений
Иногда вы можете столкнуться с ситуацией, когда вы хотите перехватить исключение, но не хотите (или хотите иметь возможность) полностью обрабатывать его в точке, где вы его отловили. Это обычное дело, когда вы хотите зарегистрировать ошибку и передать ее вызывающей функции, чтобы она ее обработала.
Когда функция может использовать код возврата, это просто. Рассмотрим следующий пример:
Database* createDatabase(std::string filename)
{
try
{
Database *d = new Database(filename);
d->open(); // предполагаем, что это вызывает исключение типа int в случае ошибки
return d;
}
catch (int exception)
{
// Создание базы данных не удалось
// Записываем ошибку в какой-нибудь глобальный лог-файл
g_log.logError("Creation of Database failed");
}
return nullptr;
}
В приведенном выше фрагменте кода функция должна создать объект Database
, открыть базу данных и вернуть объект Database
. В случае если что-то пойдет не так (например, передано неправильное имя файла), обработчик исключений регистрирует ошибку, а затем соответственно возвращает нулевой указатель.
Теперь рассмотрим следующую функцию:
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");
// Однако мы фактически не обработали эту ошибку
// Итак, что мы здесь делаем?
}
}
В случае, если эта функция завершается успешно, она возвращает значение int
– допустимым значением может быть любое целочисленное значение.
Но как насчет случая, когда с getIntValue()
что-то пойдет не так? В этом случае getIntValue()
вызовет исключение типа int
, которое будет перехвачено блоком catch
в getIntValueFromDatabase()
, который зарегистрирует эту ошибку. Но как тогда сообщить вызывающему getIntValueFromDatabase()
, что что-то пошло не так? В отличие от первого примера, здесь нет подходящего кода возврата (потому что допустимым может быть любое целочисленное возвращаемое значение).
Создание нового исключения
Одно очевидное решение – создать новое исключение.
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");
// выбросить исключение char 'q' в стек для обработки
// функцией, вызвавшей getIntValueFromDatabase()
throw 'q';
}
}
В приведенном выше примере программа перехватывает исключение int
из getIntValue()
, регистрирует ошибку, а затем генерирует новое исключение со значением char 'q'
. Хотя может показаться странным, генерировать исключение из блока catch
, но это разрешено. Помните, что могут быть перехвачены только исключения, созданные в блоке try
. Это означает, что исключение, созданное в блоке catch
, не будет перехвачено блоком catch
, в котором оно находится. Вместо этого оно будет передано вызывающей функции вверх по стеку.
Исключение, созданное из блока catch
, может быть исключением любого типа – оно не обязательно должно быть того же типа, что и исключение, которое только что было перехвачено.
Повторное выбрасывание исключения (неправильный способ)
Другой вариант – повторно выбросить то же исключение. Один из способов сделать это:
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;
}
}
Хотя это работает, у этого метода есть несколько недостатков. Во-первых, этот код не вызывает точно такое же исключение, как то, которое было перехвачено, – скорее, он выбрасывает копию переменной exception
, инициализированную копированием. Хотя компилятор может исключить это копирование, он может этого и не делать, поэтому этот подход может быть менее производительным.
Но что более важно, подумайте, что происходит в следующем случае:
int getIntValueFromDatabase(Database *d, std::string table, std::string key)
{
assert(d);
try
{
return d->getIntValue(table, key); // выдает исключение Derived в случае ошибки
}
catch (Base &exception)
{
// Записываем ошибку в какой-нибудь глобальный лог-файл
g_log.logError("doSomethingImportant failed");
// Опасность: выбрасывается объект Base, а не объект Derived
throw exception;
}
}
В этом случае getIntValue()
выбрасывает исключение с объектом Derived
, но блок catch
перехватывает ссылку типа Base
. Это нормально, поскольку мы знаем, что у нас может быть ссылка базового класса Base
на объект производного класса Derived
. Однако когда мы генерируем исключение, выброшенное исключение инициализируется копированием из переменной exception
. Переменная exception
имеет тип Base
, поэтому исключение, инициализированное копированием, также имеет тип Base
(не Derived
!). Другими словами, наш объект Derived
был обрезан!
Вы можете увидеть это в следующей программе:
#include <iostream>
class Base
{
public:
Base() {}
virtual void print() { std::cout << "Base"; }
};
class Derived: public Base
{
public:
Derived() {}
virtual void print() { std::cout << "Derived"; }
};
int main()
{
try
{
try
{
throw Derived{};
}
catch (Base& b)
{
std::cout << "Caught Base b, which is actually a ";
b.print();
std::cout << "\n";
throw b; // здесь объект Derived обрезается
}
}
catch (Base& b)
{
std::cout << "Caught Base b, which is actually a ";
b.print();
std::cout << "\n";
}
return 0;
}
Эта программа печатает:
Caught Base b, which is actually a Derived
Caught Base b, which is actually a Base
Тот факт, что вторая строка указывает, что Base b
на самом деле является Base
, а не Derived
, доказывает, что объект Derived
был обрезан.
Повторное выбрасывание исключения (правильный способ)
К счастью, C++ предоставляет способ повторно выбросить точно такое же исключение, как только что перехваченное. Для этого просто используйте ключевое слово throw
в блоке catch
(без связанной переменной), например:
#include <iostream>
class Base
{
public:
Base() {}
virtual void print() { std::cout << "Base"; }
};
class Derived: public Base
{
public:
Derived() {}
virtual void print() { std::cout << "Derived"; }
};
int main()
{
try
{
try
{
throw Derived{};
}
catch (Base& b)
{
std::cout << "Caught Base b, which is actually a ";
b.print();
std::cout << "\n";
throw; // обратите внимание: теперь здесь мы повторно выбрасываем объект b
}
}
catch (Base& b)
{
std::cout << "Caught Base b, which is actually a ";
b.print();
std::cout << "\n";
}
return 0;
}
Этот код печатает:
Caught Base b, which is actually a Derived
Caught Base b, which is actually a Derived
Это ключевое слово throw
, которое, похоже, ничего не выбрасывает, на самом деле повторно генерирует то же самое исключение, которое только что было перехвачено. Копирование не выполняется, а это значит, что нам не нужно беспокоиться о снижении производительности из-за копирования или об обрезке объекта.
Если требуется повторное выбрасывание исключения, этот метод должен быть предпочтительнее других.
Правило
При повторной генерации того же исключения используйте ключевое слово throw
без дополнительных переменных.