20.6 – Повторное выбрасывание исключений

Добавлено 7 сентября 2021 в 00:43

Иногда вы можете столкнуться с ситуацией, когда вы хотите перехватить исключение, но не хотите (или хотите иметь возможность) полностью обрабатывать его в точке, где вы его отловили. Это обычное дело, когда вы хотите зарегистрировать ошибку и передать ее вызывающей функции, чтобы она ее обработала.

Когда функция может использовать код возврата, это просто. Рассмотрим следующий пример:

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 без дополнительных переменных.

Теги

C++ / CppException / ИсключениеLearnCppДля начинающихОбработка ошибокОбучениеПрограммирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.