Исключения
Разворачивание стека, noexcept, конструкторы, деструкторы и гарантии безопасности исключений.
25 вопросов
JuniorТеорияОчень частоЧто такое stack unwinding и как RAII связан с исключениями?
Что такое stack unwinding и как RAII связан с исключениями?
Когда исключение выходит из области видимости, C++ уничтожает полностью сконструированные автоматические объекты в обратном порядке, пока не найдёт подходящий catch. RAII опирается на это: деструкторы освобождают файлы, lock, память и другие ресурсы при разворачивании стека.
Типичные ошибки
- ✗Управлять ресурсами вручную в коде, который может бросить исключение
- ✗Ловить исключения по значению и срезать производные типы
- ✗Забывать, что у частично сконструированного объекта деструктор не вызывается
Уточняющие вопросы
- →Что происходит, если конструктор бросает исключение?
- →Почему деструкторы обычно должны быть noexcept?
JuniorТеорияОчень частоПочему действует правило «бросать по значению, ловить по const-ссылке»?
Почему действует правило «бросать по значению, ловить по const-ссылке»?
Throw копирует (или перемещает) объект в рантайм-область, поэтому throw указателя на локальный — dangling, а throw базового класса по значению — срезка. Ловля по ссылке избегает срезки и копии; const показывает, что обработчик не мутирует.
Типичные ошибки
- ✗Бросать указатель на стековый объект — dangling в обработчике
- ✗Ловить по значению и срезать динамический тип до статического
- ✗Выделить исключение через
newи бросить указатель — утечка, если обработчик не сделает delete
Уточняющие вопросы
- →Что именно делает рантайм для копирования брошенного объекта?
- →Можно ли бросать тип с
noexceptmove-конструктором и получить move?
JuniorТеорияОчень частоКак работают блоки try/throw/catch? Логика и порядок catch.
Как работают блоки try/throw/catch? Логика и порядок catch.
throw expr раскручивает стек, вызывая деструкторы (RAII), пока не найдёт подходящий catch. Брошенный тип должен совпадать или публично наследоваться от пойманного. Клаузы — в порядке объявления, производные раньше баз. catch(...) ловит всё.
Типичные ошибки
- ✗Ловить по значению вместо
const&— срезает исключение при полиморфном типе - ✗Размещать
catch(std::exception&)передcatch(std::runtime_error&)— более общий обработчик перекрывает специфический - ✗Бросать исключение из деструктора при уже распространяющемся исключении — вызывает
std::terminate
Уточняющие вопросы
- →В чём разница между
throw;иthrow e;внутри catch-блока? - →Что на самом деле гарантирует спецификация
noexceptи что происходит при её нарушении?
MiddleДебаггингОчень частоЧто произойдёт, если деструктор бросит исключение во время stack unwinding?
Что произойдёт, если деструктор бросит исключение во время stack unwinding?
Если деструктор выпускает исключение наружу, пока уже разворачивается другое исключение, программа вызывает std::terminate().
Открыть задачу →Типичные ошибки
- ✗Помечать деструктор noexcept(false) и надеяться, что вызывающий код всё поймает
- ✗Бросать из cleanup-кода, когда другое исключение уже активно
- ✗Оставлять важную обработку ошибок только в деструкторе
Уточняющие вопросы
- →Что показывает std::uncaught_exceptions()?
- →Как стандартные контейнеры используют noexcept move-конструкторы?
MiddleТеорияОчень частоЧем отличаются basic, strong и nothrow exception guarantees?
Чем отличаются basic, strong и nothrow exception guarantees?
Basic: инварианты держатся, нет утечек, состояние валидно. Strong: либо завершиться, либо откатиться как транзакция. Nothrow: никогда не бросает (noexcept). vector::push_back — strong для копируемых, basic для move-only. Строится через copy-and-swap.
Типичные ошибки
- ✗Заявлять strong, а реализация при ошибке оставляет полу-изменённое состояние
- ✗Помечать функцию
noexcept, а внутри что-то бросает - ✗Забывать, что copy-and-swap требует noexcept swap
Уточняющие вопросы
- →Как
vector::push_backвыбирает между move и copy при реаллокации? - →Почему noexcept move важен для strong guarantee в контейнерах?
JuniorТеорияЧастоЧто делает catch (...) и каковы его ограничения?
Что делает catch (...) и каковы его ограничения?
catch (...) — обработчик-эллипсис, ловящий исключение любого типа, поэтому он должен быть последней клаузой. Его ограничение: нет именованного объекта, поэтому осмотреть исключение напрямую нельзя — только throw; для пере-броска или std::current_exception() для захвата.
Типичные ошибки
- ✗Ставить
catch (...)перед типизированным обработчиком — он перекрывает все клаузы после, делая их мёртвым кодом - ✗Использовать
catch (...)для тихого проглатывания всех ошибок вместо логирования или пере-броска черезthrow; - ✗Ожидать, что
catch (...)даст доступ к объекту исключения так же, как типизированный обработчик
Уточняющие вопросы
- →Как заново бросить исключение, пойманное блоком
catch (...)? - →Когда
catch (...)верхнего уровня вmain— разумная подстраховка?
JuniorТеорияЧастоМожно ли бросить исключение из конструктора? Что при этом уничтожается?
Можно ли бросить исключение из конструктора? Что при этом уничтожается?
Да. Деструктор объекта НЕ вызывается (он не был сконструирован полностью), но уже сконструированные члены и базовые классы уничтожаются в обратном порядке, а память освобождается. RAII-члены надёжно чистятся даже при частичной неудаче конструктора.
Типичные ошибки
- ✗Выделять сырую память через
newв теле конструктора перед другой потенциально бросающей операцией — выделение утекает, если последующий throw обходитdelete - ✗Ожидать, что деструктор выполнит очистку после throw в конструкторе — он не запустится; используйте RAII-члены
- ✗Бросать исключение из конструктора класса с
noexcept-конструктором — вызываетstd::terminate
Уточняющие вопросы
- →Как написать безопасный относительно исключений конструктор, захватывающий два ресурса?
- →Что такое 'function try block' на конструкторе и когда его использовать?
JuniorТеорияЧастоКакова иерархия стандартных исключений в C++ и когда от неё наследоваться?
Какова иерархия стандартных исключений в C++ и когда от неё наследоваться?
Корень std::exception с virtual const char* what(). Ветви: std::logic_error (баги предусловий), std::runtime_error (рантайм-условия). Плюс std::bad_alloc, std::bad_cast. Наследуйте от ближайшего базового класса — общий catch работает.
Типичные ошибки
- ✗Бросать сырые строки или int — нет
what(), нет общего catch - ✗Наследоваться от
std::exceptionбез переопределенияwhat()— бесполезное сообщение - ✗Путать
logic_error(это баг) сruntime_error(внешнее условие)
Уточняющие вопросы
- →Почему
std::system_errorважен для кроссплатформенного репорта ошибок? - →Что добавляет
std::nested_exception?
JuniorДизайнЧастоВы ревьюите код обработки ошибок и для каждого сбойного условия решаете, защищать ли его через assert или сигнализировать броском исключения. Объясните, как вы проводите эту границу и что это значит для release-сборок.
Вы ревьюите код обработки ошибок и для каждого сбойного условия решаете, защищать ли его через assert или сигнализировать броском исключения. Объясните, как вы проводите эту границу и что это значит для release-сборок.
assert — программистские инварианты, баги; в release исчезает. Исключения — рантайм-условия вне программы: I/O, парсинг, OOM. Правило: assert — «почини код»; exception — «мир не сотрудничает».
Типичные ошибки
- ✗Применять
assertдля валидации входных данных — исчезает в release - ✗Бросать исключение для невозможного внутреннего состояния — лучше assert и упасть рано
- ✗
assert(expr && ...)с побочными эффектами — эффекты исчезают в release
Уточняющие вопросы
- →Что добавляет proposal
contractsв C++26? - →Что такое
__builtin_unreachable()и когда его использовать?
JuniorТеорияЧастоКогда вызывается std::terminate и можно ли изменить его поведение?
Когда вызывается std::terminate и можно ли изменить его поведение?
std::terminate срабатывает в неустранимых ситуациях: исключение покинуло noexcept, throw во время раскрутки стека, нет подходящего обработчика или сбой при конструировании исключения. Он вызывает текущий обработчик — по умолчанию std::abort. Заменить его можно через std::set_terminate.
Типичные ошибки
- ✗Выпускать исключение из
noexcept-функции или деструктора и удивляться мгновенному падению - ✗Ожидать, что terminate-обработчик сможет возобновить нормальное выполнение — он обязан завершить программу
- ✗Путать
std::set_terminateсstd::set_new_handler, который решает другую проблему
Уточняющие вопросы
- →Почему terminate-обработчик не должен возвращать управление вызвавшему?
- →В чём разница между
std::terminateиstd::abort?
MiddleДизайнЧастоВы проектируете стратегию сообщения об ошибках для библиотеки и взвешиваете возврат ошибки через значение (std::error_code или std::expected из C++23) против броска исключений. Объясните, какие ситуации склоняют вас к error codes, а не к исключениям, и почему.
Вы проектируете стратегию сообщения об ошибках для библиотеки и взвешиваете возврат ошибки через значение (std::error_code или std::expected из C++23) против броска исключений. Объясните, какие ситуации склоняют вас к error codes, а не к исключениям, и почему.
Исключения — для действительно исключительных условий через много фреймов. Error codes / std::expected (C++23) — горячие пути, API в циклах, -fno-exceptions (embedded, ядро), либо когда неудача — штатный поток.
Типичные ошибки
- ✗Бросать при ошибке парсинга в горячем цикле — сильно бьёт по производительности
- ✗Возвращать bool и out-параметр для результата —
std::optionalилиstd::expectedпонятнее - ✗Ловить всё как
std::exception, теряя конкретный error code
Уточняющие вопросы
- →Что даёт
std::expected<T, E>сверхstd::variant<T, E>? - →Как
std::error_categoryпозволяет расширять error codes?
MiddleТеорияЧастоЧто такое гарантии безопасности исключений? Какие предоставляют контейнеры STL?
Что такое гарантии безопасности исключений? Какие предоставляют контейнеры STL?
Четыре уровня: без гарантии, базовая (нет утечек, состояние валидно), строгая (commit-or-rollback), no-throw (noexcept). STL даёт строгую для одноэлементных операций вроде push_back, базовую для многоэлементных.
Типичные ошибки
- ✗Думать, что RAII один даёт строгую гарантию — RAII предотвращает утечки (базовая), но откат требует copy-and-swap или аналога
- ✗Не помечать конструкторы перемещения
noexcept—std::vectorоткатывается к копированию при перевыделении памяти, теряя преимущество перемещения - ✗Считать все операции STL строго безопасными относительно исключений — например,
std::vector::insertв середину даёт только базовую гарантию
Уточняющие вопросы
- →Как idiom copy-and-swap реализует строгую гарантию безопасности исключений?
- →Какую гарантию предоставляет
std::map::operator[]при вставке нового ключа?
MiddleТеорияЧастоЧто произойдёт, если исключение покинет noexcept-функцию?
Что произойдёт, если исключение покинет noexcept-функцию?
Если исключение выходит из noexcept-функции, немедленно работает std::terminate — стек может не раскручиваться, деструкторы могут не выполниться, не перехватываемо. Замысел: компиляторы оптимизируют (особенно move), убирая таблицы.
Типичные ошибки
- ✗Помечать деструктор
noexcept(false)— деструкторы неявноnoexceptс C++11; явный отказ почти всегда неверен - ✗Вызывать потенциально бросающую функцию внутри
noexcept-функции без try/catch — при исключении вызываетсяterminate - ✗Считать
noexceptгарантией времени выполнения — это контракт; нарушение вызывает terminate, а не перехватываемое исключение
Уточняющие вопросы
- →Почему
std::vectorиспользует конструктор перемещения только если онnoexcept? - →Что такое
noexcept(noexcept(expr))и когда это пишут?
MiddleТеорияЧастоЧто делает голый throw; в блоке catch?
Что делает голый throw; в блоке catch?
Голый throw; пере-бросает активное исключение без копии — сохраняется исходный динамический тип, что важно при пере-броске через catch базового класса. throw e; копирует e и срезает до статического типа, если e — значение.
Типичные ошибки
- ✗Писать
throw e;вместоthrow;в catch — копирует и может срезать исключение - ✗Использовать голый
throw;вне контекста catch — вызываетstd::terminate - ✗Не знать, что
std::current_exceptionзахватывает активное исключение для последующей переброски черезstd::rethrow_exception
Уточняющие вопросы
- →Когда вы использовали бы
std::current_exceptionиstd::rethrow_exception? - →Как добавить контекст к исключению (залогировать сообщение) и затем перебросить его?
JuniorТеорияИногдаКак обработать деление на ноль в C++?
Как обработать деление на ноль в C++?
Целочисленное деление на ноль — UB: упадёт или SIGFPE, НЕ ловится try/catch. Проверяйте: if (b == 0) throw std::domain_error(...);. Вещественное по IEEE 754 даёт +inf, -inf или NaN, сигнала нет.
Типичные ошибки
- ✗Пытаться поймать целочисленное деление на ноль через
catch(...)— сигнал SIGFPE не является исключением C++ - ✗Не различать семантику целочисленного и вещественного деления
- ✗Использовать try/catch для 'обработки' вещественного NaN — NaN распространяется молча; проверяйте через
std::isnan
Уточняющие вопросы
- →Как включить исключения плавающей точки (FE_DIVBYZERO, FE_INVALID) для обнаружения ошибочных операций?
- →Что возвращает
std::numeric_limits<double>::infinity()и когда оно производится?
MiddleТеорияИногдаКогда new бросает std::bad_alloc и как обрабатывать out-of-memory в C++?
Когда new бросает std::bad_alloc и как обрабатывать out-of-memory в C++?
Стандартный new бросает std::bad_alloc; new(std::nothrow) T возвращает nullptr. На Linux с overcommit OOM-killer срабатывает позже, ловить ненадёжно. Стратегии: предвыделять, set_new_handler или неброские аллокаторы.
Типичные ошибки
- ✗Ловить
bad_allocи снова аллоцировать в handler'е — скорее всего, тоже упадёт - ✗Выделять большие буферы в handler'е — тот же OOM
- ✗Доверять
bad_allocна Linux безvm.overcommit_memory=2
Уточняющие вопросы
- →Как работает
std::set_new_handler? - →Почему overcommit — проблема для детекции OOM?
MiddleТеорияИногдаЧто такое std::expected и как он соотносится с бросанием исключения?
Что такое std::expected и как он соотносится с бросанием исключения?
std::expected<T, E> (C++23) хранит либо значение, либо ошибку E внутри объекта, без аллокации и раскрутки стека. Неудача — обычный возврат, проверяемый через has_value(). В отличие от исключений, путь ошибки стоит столько же, сколько успех, и виден в сигнатуре.
Типичные ошибки
- ✗Вызывать
value()уexpectedбез предварительной проверкиhas_value()— бросаетbad_expected_access - ✗Применять
expectedдля действительно исключительных сбоев, где исключение распространяется чище - ✗Полностью игнорировать возвращённый
expected, из-за чего ошибка молча исчезает
Уточняющие вопросы
- →Как
and_thenиtransformцепочкой обрабатываютstd::expected? - →Что даёт
std::expectedсверх возвратаstd::optional<T>?
MiddleТеорияИногдаКак компилятор сопоставляет брошенное исключение с несколькими catch?
Как компилятор сопоставляет брошенное исключение с несколькими catch?
Обработчики catch проверяются в порядке записи, не по совпадению — первый тип, принимающий throw, побеждает. Производные классы раньше базовых, иначе базовый перехватит, а производный — мёртвый код. catch(...) всегда последний.
Типичные ошибки
- ✗Ставить
catch(const std::exception&)доcatch(const std::runtime_error&)— runtime_error недостижим - ✗Использовать
catch(...)для глушения всех ошибок — прячет баги - ✗Забывать, что указательные иерархии подчиняются тем же правилам порядка
Уточняющие вопросы
- →Почему стандарт требует сопоставление по порядку, а не самый производный?
- →Как пере-бросить исключение, пойманное
catch(...)?
MiddleТеорияИногдаКак эволюционировали спецификации исключений от throw() к noexcept?
Как эволюционировали спецификации исключений от throw() к noexcept?
Старые динамические спецификации вроде throw(int) проверялись в рантайме и вызывали std::unexpected при нарушении — дорого и почти бесполезно. C++11 добавил noexcept (контракт без бросков), объявил динамические спецификации устаревшими, приравнял throw() к noexcept, а C++17 удалил их полностью.
Типичные ошибки
- ✗Писать
throw(SomeType)в современном коде — удалено в C++17, не скомпилируется - ✗Считать, что
throw()иnoexceptразличаются в поведении при нарушении — оба в итоге вызываютstd::terminate - ✗Полагать, что динамические спецификации когда-либо проверялись на этапе компиляции, а не в рантайме
Уточняющие вопросы
- →Почему комитет признал динамические спецификации исключений провальным дизайном?
- →Как
noexceptвключает оптимизацию move-или-copy вstd::vector?
SeniorПроизводительностьИногдаКакова рантайм-стоимость исключений, когда они не бросаются и когда бросаются?
Какова рантайм-стоимость исключений, когда они не бросаются и когда бросаются?
На Itanium ABI (Linux/macOS) путь без throw почти бесплатен. Throw дорог: DWARF, деструкторы, аллокация объекта. Windows SEH — малый per-function setup. Throw в ~10-1000× медленнее error code.
Типичные ошибки
- ✗Бросать в горячем цикле и удивляться падению throughput
- ✗Отключать исключения глобально ради экономии — а её нет
- ✗Сравнивать стоимость throw с одним вызовом, а не с эквивалентной error-code пропагацией
Уточняющие вопросы
- →Что такое
-fno-exceptionsи когда это уместно? - →Как работает Itanium zero-cost (personality routine, LSDA)?
MiddleДебаггингРедкоМожно ли поймать stack overflow в C++ через try/catch?
Можно ли поймать stack overflow в C++ через try/catch?
Нет. Stack overflow — сигнал ОС (SIGSEGV на Linux, access violation на Windows), не C++ исключение. Рантайм не может раскрутить стек без места. Windows SEH умеет перехватить page fault, но это не переносимый C++.
Типичные ошибки
- ✗Писать
try { recurse(); } catch (...) {}и ждать обработки stack overflow - ✗Выделять большие массивы на стеке в глубоко вложенных вызовах
- ✗Путать stack overflow с
std::bad_alloc— разные механизмы
Уточняющие вопросы
- →Чем Windows SEH отличается от C++ исключений?
- →Что такое guard page и как ОС детектирует overflow?
SeniorТеорияРедкоЧто такое std::exception_ptr и как передать исключение между потоками?
Что такое std::exception_ptr и как передать исключение между потоками?
std::exception_ptr — shared type-erased smart pointer на копию исключения. В catch current_exception() возвращает его; rethrow_exception(p) пере-бросает в другом месте. std::promise::set_exception доставляет ошибки worker'а в future.
Типичные ошибки
- ✗Хранить
std::exception_ptrи пытаться разобрать безrethrow_exception+ catch - ✗Не обрабатывать случай null ptr (нет активного исключения)
- ✗Звать
current_exception()вне handler'а и ждать не-null
Уточняющие вопросы
- →Какова стоимость
current_exception()(аллокация, копия)? - →Как
std::futureпередаёт исключения worker'а?
SeniorТеорияРедкоЧто такое function-try-block и для чего он специально нужен?
Что такое function-try-block и для чего он специально нужен?
Function-try-block оборачивает всё тело функции, а в конструкторах — ещё и список инициализации членов. Уникальная польза — ловить throw из инициализации членов. Handler может почистить, но не подавить — рантайм пере-бросит.
Типичные ошибки
- ✗Пытаться подавить исключение в function-try-block конструктора — рантайм всё равно rethrow
- ✗Применять function-try-block к свободной функции как будто это что-то меняет — не меняет
- ✗Обращаться к членам
thisв catch конструктора — они могут быть частично сконструированы
Уточняющие вопросы
- →Почему catch конструктора не может подавить исключение?
- →Как function-try-block работает с делегирующими конструкторами?
SeniorТеорияРедкоЧто такое std::nested_exception и как использовать throw_with_nested / rethrow_if_nested?
Что такое std::nested_exception и как использовать throw_with_nested / rethrow_if_nested?
std::nested_exception прикрепляет захваченное прежнее исключение к новому, сохраняя цепочку причин. throw_with_nested(e) бросает e с nested_exception и активным исключением. rethrow_if_nested(p) пере-бросает причину.
Типичные ошибки
- ✗Забывать
dynamic_castдоnested_exceptionпри ручном раскрытии цепочки - ✗Звать
throw_with_nestedвне активного handler'а — текущего исключения нет - ✗Логировать только внешний
what()— теряется цепочка
Уточняющие вопросы
- →Как
std::current_exceptionпредставляет исключение (std::exception_ptr)? - →Когда передавать
exception_ptrмежду потоками?
SeniorТеорияРедкоЧто возвращает std::uncaught_exceptions() и зачем оно нужно?
Что возвращает std::uncaught_exceptions() и зачем оно нужно?
std::uncaught_exceptions() (C++17) возвращает количество исключений в полёте на этом потоке. Используется в деструкторах для детекта раскрутки — ScopeGuard откатывает, если счётчик выше, чем при конструировании.
Типичные ошибки
- ✗Использовать
uncaught_exception()(единственное число, устаревшее) — ломается при вложенных исключениях - ✗Звать вне деструктора и принимать решения по результату — хрупко
- ✗Бросать из деструктора на основе результата — обычно всё равно terminate
Уточняющие вопросы
- →Как ScopeGuard Андрея Александреску использует это?
- →Почему
uncaught_exception()заменили на множественную форму?