20.7 – Функциональные блоки try
Блоки 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
полезен в первую очередь либо для логгирования сбоев перед передачей исключения вверх по стеку, либо для изменения типа генерируемого исключения.