20.7 – Функциональные блоки try

Добавлено 12 сентября 2021 в 11:51

Блоки try и catch в большинстве случаев работают достаточно хорошо, но есть один конкретный случай, в котором их недостаточно. Рассмотрим следующий пример:

class A
{
private:
    int m_x;
public:
    A(int x) : m_x{x}
    {
        if (x <= 0)
            throw 1;
    }
};

class B : public A
{
public:
    B(int x) : A{x}
    {
        // Что произойдет, если создание A не удастся, и мы хотим обработать это здесь?
    }
};

int main()
{
    try
    {
        B b{0};
    }
    catch (int)
    {
        std::cout << "Oops\n";
    }
}

В приведенном выше примере производный класс B вызывает конструктор базового класса A, который может вызвать исключение. Поскольку создание объекта b было помещено в блок try (в функции main()), если A выдает исключение, блок try в main его перехватит. Следовательно, эта программа печатает:

Oops

Но что, если мы хотим поймать это исключение внутри B? Вызов конструктора базового класса A происходит через список инициализации членов до вызова тела конструктора B. И его невозможно обернуть стандартным блоком try.

В этой ситуации мы должны использовать слегка измененный блок try, называемый функциональным блоком try.

Функциональные блоки try

Функциональные блоки try предназначены для того, чтобы позволить вам установить обработчик исключений вокруг тела всей функции, а не вокруг блока кода.

Синтаксис функциональных блоков try немного сложно описать, поэтому мы покажем его на примере:

#include <iostream>

class A
{
private:
    int m_x;
public:
    A(int x) : m_x{x}
    {
        if (x <= 0)
            throw 1;
    }
};

class B : public A
{
public:
    B(int x) try : A{x} // обратите внимание на добавление здесь ключевого слова try
    {
    }
    catch (...) // обратите внимание, что это на том же уровне отступа, что и сама функция
    {
        // Здесь перехватываются исключения из списка инициализаторов членов
        // и тела конструктора

        std::cerr << "Exception caught\n";

        // Если здесь исключение не было сгенерировано явно,
        // текущее исключение будет неявно выброшено повторно
    }
};

int main()
{
    try
    {
        B b{0};
    }
    catch (int)
    {
        std::cout << "Oops\n";
    }
}

Когда эта программа запускается, она выдает следующее:

Exception caught
Oops

Разберем эту программу подробнее.

Во-первых, обратите внимание на добавление ключевого слова try перед списком инициализаторов элементов. Оно указывает на то, что всё, что находится после этой точки (до конца функции), следует рассматривать как расположенное внутри блока try.

Во-вторых, обратите внимание, что связанный блок catch находится на том же уровне отступа, что и вся функция. Здесь будет обнаружено любое исключение, возникшее между ключевым словом try и концом тела функции.

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

В приведенной выше программе, поскольку мы явно не выбросили исключение в блоке catch на уровне функции, исключение было неявно повторно выброшено и было перехвачено блоком catch в main(). По этой причине программа напечатает "Oops"!

Хотя блоки try на уровне функций можно использовать и с функциями, не являющимися членами классов, обычно так не делается, потому что это редко бывает необходимо. Они почти всегда используются только с конструкторами!

Функциональные блоки try могут перехватывать исключения как базового, так и текущего класса

В приведенном выше примере, если конструкторы A или B генерируют исключение, оно будет перехвачено блоком try вокруг конструктора B.

Мы можем увидеть это в следующем примере, где мы генерируем исключение в классе B вместо класса A:

#include <iostream>

class A
{
private:
    int m_x;
public:
    A(int x) : m_x{x}
    {
    }
};

class B : public A
{
public:
    B(int x) try : A{x} // обратите внимание на добавление здесь ключевого слова try
    {
        if (x <= 0)  // это перемещено из A в B
            throw 1; // и это тоже
    }
    catch (...)
    {
        std::cerr << "Exception caught\n";

        // Если здесь исключение не было сгенерировано явно,
        // текущее исключение будет неявно выброшено повторно
    }
};

int main()
{
    try
    {
        B b{0};
    }
    catch (int)
    {
        std::cout << "Oops\n";
    }
}

И мы получаем тот же результат:

Exception caught
Oops

Не используйте функциональный блок try для освобождения ресурсов

При сбое построения объекта деструктор класса не вызывается. Следовательно, у вас может возникнуть соблазн использовать функциональный блок try как способ очистки класса, которому частично были выделены ресурсы, прежде чем он выдаст сбой. Однако обращение к членам давшего сбой объекта считается неопределенным поведением, поскольку объект будет «мертв» еще до выполнения блока catch. Это означает, что вы не можете использовать функциональный блок try для выполнения очистки после класса. Если вы хотите выполнить очистку после класса, следуйте стандартным правилам очистки для классов, которые вызывают исключения (смотрите подраздел «Когда конструкторы дают сбой» в уроке «20.5 – Исключения, классы и наследование»).

Функциональный блок try полезен в первую очередь либо для логгирования сбоев перед передачей исключения вверх по стеку, либо для изменения типа генерируемого исключения.

Теги

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

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

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