[Перевод] С++ exception handling под капотом или как же работают исключения в C++
От переводчика
В мире победили языки высокого уровня и в мирах руби-питон-js разработчиков остается только разглагольствовать, что в плюсах не стоит использовать то или иное. Например, исключения, потому что они медленные и генерируют много лишнего кода. Стоило спросить «и какой же код он генерирует», как в ответ получил мямленье и мычание. А и правда — как же они работают? Ну что ж, компилируем в g++ с флагом -S, смотрим что получилось. Поверхностно разобраться не сложно, однако то, что остались недопонимания — не давали мне спать. К счастью, готовая статья нашлась.
На хабре есть несколько статей, подробных и не очень (при этом все равно хороших), посвященных тому, как работают exceptions в C++. Однако нет ни одной по-настоящему глубокой, поэтому я решил восполнить этот пробел, благо есть подходящий материал. Кому интересно как работают исключения в C++ на примере gcc — запаситесь pocket-ом или evernot, свободным временем и добро пожаловать под кат.
2 часть
3 часть
P.S. Пару слов о переводе:
- перевод очень очень близкий к тексту, но иногда я позволял себе изменять целые абзацы
- некоторые термины я так и не придумал, как перевести, например landing pad и call site
- Работы оказалось гораздо больше, чем казалось, к концу я даже стал путать, где перевод, а где оригинал, некоторые строки были написаны в 4 ночи, в общем — если где-то будут несвязные слова или целые предложения — простите, в ближайшее время постараюсь все подправить.
- В данном случае код является неотъемлемой частью статьи, поэтому прятать под спойлер ничего не буду.
- Как всегда, орфография, пунктуация и мелкие ошибки — в личку. Фактические ошибки, неточности и недоработки — в комментарии.
Все знают, что обработка исключений трудна. Причин для этого предостаточно, в каждом слое «жизненного цикла» исключения: сложно писать код с сильной гарантией безопасности по исключениям (exception safe code), исключения могут выбрасываться из неожиданных мест, может оказаться проблематичным попытка понять плохо спроектированную иерархию исключений, это медленно работает из-за большого количества вуду-магии под капотом, это опасно, потому что неправильное пробрасывание ошибки может привести к непростительному вызову std::terminate
. И, несмотря на все это, битвы по поводу использовать или нет исключения в программах все еще продолжаются. Вероятно, это из-за неглубокого понимания как же они работают.
Для начала нужно спросить себя: как это все работает? Это первая статься из длинной серии, которую я пишу о том, как реализованы исключения под копотом в C++ (под платформу gcc под x86, но должно быть применимо для других платформ так же). В этих статьях процесс выброса и отлова ошибок будет объяснено во всех подробностях, но для нетерпеливых: короткий бриф всех статей о пробросе исключений в gcc/x86:
- Когда мы пишем throw операратор, компилятор транслирует его в пару вызовов
libstdc++
функций, которые размещают исключение и начинаются быстрый процесс раскручивания стека вызовом библиотекиlibstdc
. - Для каждого catch блока компилятор дописывает некую специальную информацию после тела метода, таблицу исключений, которые метод может отлавливать, а так же таблицу очистки (подробнее о таблице очистки будет далее).
- В процессе раскручивания стека вызывается специальная функция, поставляемая
libstdc++
(называемая «персональной подпрограммой» [personality routine]), которая проверяет каждую функцию в стеке на ошибки, которые она может отлавливать. - Если не нашлось никого, кто мог бы отловить эту ошибку, вызывается
std::terminate
. - Если кто-то все же нашелся, раскрутка запускается снова с вершины стека.
- При повторном проходе по стеке запускается персональная подпрограмма по очистке ресурсов для каждого метода.
- Подпрограмма проверяет таблицу очистки для текущего метода. Если в ней есть, что очистить, рутина
прыгает
в текущий фрейм стэка и запускает код очистки, который вызывает деструкторы для каждого из объектов, размещенных в текущекей облсти видимости. - Когда раскрутка натыкается на фрагмент стека, который может обрабатывать исключение, она «прыгает» в блок обработки исключения.
- После окончания обработки исключения, функция очистки вызывается для освобождения памяти, занятой исключением.
- У нас это будет одна большая статья, побитая на части, поэтому далее «серия статей» будет заменяться просто на «статью», чтобы не загромождать лишним.
Даже сейчас это выглядит сложно, а ведь мы даже не начали, это было лишь короткое и неточное описание сложностей, необходимых для обработки исключений.
Для изучения всех деталей, происходящих под копотом, в следующей части мы начнем с реализации собственной мини-версии libstdlibc++
. Не всю, только часть с обработкой ошибок. В реальности даже не всю эту часть, лишь необходимый минимум для реализации throw/catch блока. Так же понадобится немного ассемблера, но лишь совсем совсем немного. Зато понадобится много терпения, к сожалению.
Если вы слишком любопытны, можете начинать тут. Это — полная спецификация того, что мы будем реализовывать в следующих частях. Я же попытаюсь сделать эту статью поучительной и более проститой, чтобы следующий раз вам было проще начинать с вашим собственным ABI (application binary interface, Двоичный интерфейс приложений — прим. переводчика).
Причемения (отказ от ответственности):
Я ни в коей мере не сведую в том, какая вуду-магия происходит, когда пробрасывается исключение. В этой статье я попытаюсь разоблачить тайное и узнать, как же оно устроено. Какие-то мелочи и тонкости будут не соответствовать действительности. Пожалуйста, дейте мне знать, если где-то что-то неправильно.
Прим. переводчика: это актуально и для перевода.
Если мы пытаемся понять, почему исключения такие сложные и как они работают, мы можем либо утонуть в тоннах мануалов и документаций, либо попытаться отловить исключения самостоятельно. В действительности, я был удивлен отстутствием качественной информации по теме (прим. переводчика — я, к слову, тоже): все, что можно найти либо через чур детально, либо слишком уж простое. Конечно же, есть спецификации (наиболее документировано: ABI for C++, но так же CFI, DWARF и libstdc), но обособленное чтение документации не достаточно, если вы дейтсвительно хотите понять, что происходит внутри.
Давай те начнем с очевидного: с переизобретения колеса! Мы знае факт, что чистый C не позволяет отслеживать исключения, так что давай те попытаемся слинковать C++ программу линкером чистого C и посмотрим, что произойдет! Я начал с чего-то простого типа этого:
#include "throw.h"
extern "C" {
void seppuku() {
throw Exception();
}
}
Не забудте external
, иначе G++ услужливо выпилит нашу небольшую функцию и сделает невозможным для линковки с нашей программой на чистом C. Конечно же, нам нужен заголовчный файл для линковки (не каламбур), чтобы сделать возможным соединить миры C++ и C:
struct Exception {};
#ifdef __cplusplus
extern "C" {
#endif
void seppuku();
#ifdef __cplusplus
}
#endif
И очень простой main:
#include "throw.h"
int main()
{
seppuku();
return 0;
}
Что случится, если мы попытаемся скомпилировать и слинковать этот франкинкод (от франкинштейн — прим. перев.)?
> g++ -c -o throw.o -O0 -ggdb throw.cpp
> gcc -c -o main.o -O0 -ggdb main.c
Заметка: вы можете загрузить весь исходный код для этого проекта с моего гит-репозитория.
Пока что все хорошо. Оба g++ и gcc счастливы в своем маленьком мире. Хаос начнется как только мы попроуем их слинковать вместе:
> gcc main.o throw.o -o app
throw.o: In function `foo()':
throw.cpp:4: undefined reference to `__cxa_allocate_exception'
throw.cpp:4: undefined reference to `__cxa_throw'
throw.o:(.rodata._ZTI9Exception[typeinfo for Exception]+0x0): undefined reference to `vtable for __cxxabiv1::__class_type_info'
collect2: ld returned 1 exit status
Ну и конечно же, gcc жалуется на недостающие C++ объявления. Это очень специфичные C++ объявления. Посмотрите на последнюю строку ошибки: пропущена vtable
для cxxabiv1
. cxxabi
, объявленная в libstdc++
, ссылается на ABI для C++. Теперь мы знаем, что обработка ошибок выполняется с помощью стандартной C++ библиотекой с объявленным интерфейсом C++ ABI.
C++ ABI объявляет стандартный бинарны формат, с помощью которого мы можем слинковать объекты вместе в одной программе. Если мы скомпилируем .o файлы с двумя разными компиляторами и эти компиляторы используют разные ABI, мы не сможет объеденим их в одно приложение. ABI может так же объявлять разные другие стандарты, например, интерфейс для раскручивания стека или пробрасывания исключения. В этом случае ABI определяет интерфейс (не обязательно бинарный формат, просто интерфейс) между C++ и другими библиотеками в нашем приложении, которые обеспечивают раскрутку стэка. Иными словами — ABI определяет специфичные для C++ вещи, благодаря которым наше приложение может общаться с не-C++ библиотеками: это то, что позволит пробрасыват исключения из других языков, которые будут отловлены в C++, ну и множество других вещей.
В любом случае, ошибки линкера — точка отправляние и первый слой в работе исключений под капотом: интерфейс, который нам нужно реализовать — cxxabi
. В следующей части мы начнем с собственого мини-ABI, определенного в точности как C++ ABI.
В нашем путеществии в понимании исключений мы открыли, что вся тяжелая атлетика реализована в libstdc++
, определение которой дано в C++ ABI. Просматривая ошибки линкера мы вывели, что для обработки ошибок мы должны обратиться за помощью к C++ ABI; мы создали плюющуюся ошибками C++ программу, слинковали вместе с программой на чистом C и обнаружили, что компилятор каким-то образом транслирцет наши throw инструкции в что-то, что теперь вызывает несколько libstd++ функции, которые непосредственно выбрасывают исключение.
Тем не менее, вы хотим понять как именно работают исключения, так что мы попробуем реализовать собственный mini-ABI, обеспечивающий механизм пробрасывания ошибок. Чтобы сделать это, нам понадобится лишь RTFM, однако полный интерфейс может быть найден тут, для LLVM. Давай те ка вспомним, каких конкретно функций недостает.
> gcc main.o throw.o -o app
throw.o: In function `foo()':
throw.cpp:4: undefined reference to `__cxa_allocate_exception'
throw.cpp:4: undefined reference to `__cxa_throw'
throw.o:(.rodata._ZTI9Exception[typeinfo for Exception]+0x0): undefined reference to `vtable for __cxxabiv1::__class_type_info'
collect2: ld returned 1 exit status
__cxa_allocate_exception
Имя самодостаточно, я полагаю. __cxa_allocate_exception принимает size_t и выделяет достаточно памяти для хранения исключения во время его пробрасывания. Это сложнее, чем вы ожидаете: когда ошибка обрабатывается, происходит некая магия со стеком, аллоцирование (прим. переводчика — да простите за это слово, но иногда я буду его использовать) в стеке — плохая идея. Выделение памяти в куче (heap) так же плохая идея, потому что где мы будем выделять память при исключении, сигнализирующем о том, что память закончилась? Статичное (static) размещение в памяти так же плохая идея, покуда нам нужно сделать это потокобезопасным (иначе два конкурирующих потока, выбросившие исключения будут равны катострофе). Учитывая все эти проблемы, наиболее выгодным выглядит выделение памяти в локальном хранилище потока (куче), однако при необходимости обращаться к аварийному хранилищу (предположительно, статичному), если память закончилась (out of memory). Мы, конечно же, не будем волноваться по поводу страшных деталей, так что мы можем просто использовать ститичный буффер, если будет нужно.
__cxa_throw
Эта функция делает всю магию пробрасывания! Согласно ABI, как только исключение было создано, __cxa_throw должен быть вызван. Эта функция ответственна за вызов раскрутки раскрутки стэка. Важный эффект: _cxa_throw никогда не предполагает возврат (return). Он так же передает управление подходящему catch-блок для обработки исключения либо вызывает (по-умолчанию) std::terminate
, но никогда ничего не возвращает.
vtable for cxxabiv1:: class_type_info
Странно… __class_type_info это явно какая-то RTTI (run-time type information, run-time type identification, Динамическая идентификация типа данных), но какая именно? Не просто ответить на этот вопрос сейчас, да и это не адски важно для нашего мини-ABI; оставим это части «приложение», которую мы приведем после завершения анализа процесса пробрасывания исключения, сейчас же давай те просто скажем, что это — точка входа определения ABI в рантайме, отвечающая «эти два типа одинаковы или нет». Эта функция, которая вызывается, чтобы определить: может ли данный catch-блок обрабатывать эту ошибку или нет. Сейчас мы сфокусируемся на основном: нам необходимо дать её как адрес для линкера (т.е. определить её не достаточно, нужно еще её инициировать) и она должна иметь vtable (да да, она должна иметь виртуальный метод).
Много работы происходит в этих функциях, но давайте попробуем реализовать простейший метатель исключений: тот, который будет делать выход из программы (call exit), когда исключение выброшено. Наше приложение почти завершено, но пропущены некоторые ABI-функции, так что давайте создадим mycppabi.cpp. Читая нашу ABI-спецификацию мы можем изобразить наши сигнатуры для __cxa_allocate_exception и __cxa_throw:
#include
#include
#include
namespace __cxxabiv1 {
struct __class_type_info {
virtual void foo() {}
} ti;
}
#define EXCEPTION_BUFF_SIZE 255
char exception_buff[EXCEPTION_BUFF_SIZE];
extern "C" {
void* __cxa_allocate_exception(size_t thrown_size)
{
printf("alloc ex %i\n", thrown_size);
if (thrown_size > EXCEPTION_BUFF_SIZE) printf("Exception too big");
return &exception_buff;
}
void __cxa_free_exception(void *thrown_exception);
#include
void __cxa_throw(
void* thrown_exception,
struct type_info *tinfo,
void (*dest)(void*))
{
printf("throw\n");
// __cxa_throw never returns
exit(0);
}
} // extern "C"
Напомню: вы можете найти исходники в моем github репозитории.
Если сейчас скомпилировать mycppabi.cpp и слинковать с другими двумя .o файлами, мы получим работающие бинарники, которые должны вывести «alloc ex 1\n throw» и, после этого, выйти. Очень просто, но, тем не менее, удивительно: мы управляем исключениями без вызова libc++, Мы написали (очень очень маленькую) часть C++ ABI!
Другая важная часть мудрости, полученная нами из создания нашего собственного мини-ABI: ключевое слово throw
комилируется в два вызова функций из libstdc++. Здесь нет никакой вуду-магии, это простая трансормация. Мы может даже дизассемблировать нашу функцию для проверки этого. Запустим g++ -S throw.cpp
seppuku:
.LFB3:
[...]
call __cxa_allocate_exception
movl $0, 8(%esp)
movl $_ZTI9Exception, 4(%esp)
movl %eax, (%esp)
call __cxa_throw
[...]
Даже больше магии: когда throw
транслируется в эти два вызова, компилятор даже не знаю, как исключение собирается обрабатываться. Как только libstdc++
определяет __cxa_throw
и её друзей, libstdc++ динамически линкуется в рантайме, метод обработки исключений может быть выбран когда мы первый раз запускаем приложение.
Мы уже видим прогресс, но нам еще стоит пройти огромный путь познания. Наш ABI может только выбрасывать исключения сейчас. Можем ли мы его расширить его, чтобы он отлавливал ошибки? Что ж, посмотри как это сделать в следующей части!
В этой статье мы открыли немного о пробросе исключений, наблюдая за ошибками компилятора и линкера, но мы до сих пор далеки до изучения чего-либо об отловек ошибок. Подведем итоге нескольких вещей, которые мы уже выяснили:
- throw-объявление будет транслировано компилятором в два вызова: __cxa_allocate_exception и __cxa_throw.
- __cxa_allocate_exception и __cxa_throw «живут» в
libstdc++
. - __cxa_allocate_exception выделяет память для нового исключения.
- __cxa_throw подготавливает работу и переводи исключение в _Unwind, набор функций, которые живут в
libstdc
и производит реальное разворачивание стэка (ABI определяет интерфейс этих функций).
До сих пор было достаточно просто, но отлов исключений немного сложнее, особенно потому это требует немного рефлексии (reflexion) (она позволяет программе анализировать свой собственный код). Давай те воспользуемся нашим старым методом и добавим какой-нибудь catch-блок в наш код, скомпилируем и посмотрим, что произойдет:
#include "throw.h"
#include
// Добавляем второй тип исключений
struct Fake_Exception {};
void raise() {
throw Exception();
}
// Анализируем, что произойдет, если исключение не отлавливается в catch-блок
void try_but_dont_catch() {
try {
raise();
} catch(Fake_Exception&) {
printf("Running try_but_dont_catch::catch(Fake_Exception)\n");
}
printf("try_but_dont_catch handled an exception and resumed execution");
}
// И что произойдет, если отлавилвается
void catchit() {
try {
try_but_dont_catch();
} catch(Exception&) {
printf("Running try_but_dont_catch::catch(Exception)\n");
} catch(Fake_Exception&) {
printf("Running try_but_dont_catch::catch(Fake_Exception)\n");
}
printf("catchit handled an exception and resumed execution");
}
extern "C" {
void seppuku() {
catchit();
}
}
Как и ранее, мы имеем функцию seppuku, соединяющую C и C++ миры, только в этот раз мы добавили несколько вызовов функций чтобы сделать наш стэк более интересным, так же мы добавили ветви try/catch блоков, так что теперь мы можем анализировать как libstdc++ обрабатывает их.
Как и раньше, мы получаем ошибки линковщика о отсутствующих ABI-функциях:
> g++ -c -o throw.o -O0 -ggdb throw.cpp
> gcc main.o throw.o mycppabi.o -O0 -ggdb -o app
throw.o: In function `try_but_dont_catch()':
throw.cpp:12: undefined reference to `__cxa_begin_catch'
throw.cpp:12: undefined reference to `__cxa_end_catch'
throw.o: In function `catchit()':
throw.cpp:20: undefined reference to `__cxa_begin_catch'
throw.cpp:20: undefined reference to `__cxa_end_catch'
throw.o:(.eh_frame+0x47): undefined reference to `__gxx_personality_v0'
collect2: ld returned 1 exit status
Мы опять видим кучу всего интересного. Вызов __cxa_begin_catch и __cxa_end_catch мы ожидали, хоть пока и не знаем, что они такое, но можем предположить, что они эквивалентны throw/__cxa_allocate/throw конвенция). __gxx_personaluty_v0 — что-то новое, и оно и будет основной темой следующих частей.
Что делает персональная функция? (при. переводчика — не придумал лучшего названия, подскажите в комментариях, если есть идеи). Мы уже что-то говорили о ней в введении, однако следующий раз мы посмотрим на нее гораздо детальнее, как и на наших двух новых друзей: __cxa_begin_catch и __cxa_end_catch.
После изучения того, как исключения выбрасываются, мы оказались на пути изучения как они отлавливаются. В предыдущей главе мы добавили в наш пример приложения мы добавили try-catch-блок чтобы увидеть что делает компилятор, а так же получили ошибки линкера прямо как в прошлый раз, когда мы смотрели, что произойдет если добавить throw-блок. Вот что линкер выводит:
> g++ -c -o throw.o -O0 -ggdb throw.cpp
> gcc main.o throw.o mycppabi.o -O0 -ggdb -o app
throw.o: In function `try_but_dont_catch()':
throw.cpp:12: undefined reference to `__cxa_begin_catch'
throw.cpp:12: undefined reference to `__cxa_end_catch'
throw.o: In function `catchit()':
throw.cpp:20: undefined reference to `__cxa_begin_catch'
throw.cpp:20: undefined reference to `__cxa_end_catch'
throw.o:(.eh_frame+0x47): undefined reference to `__gxx_personality_v0'
collect2: ld returned 1 exit status
Напомню, что код вы можете получить на моем гит-репозитории.
В теории (в нашей теории, разумеется), catch-блок транслируется в пару __cxa_begin_catch/end_catch из libstdc++, но и во что-то новое, называемое персональной функцией, о который мы пока еще ничего не знаем.
Давайте проверим нашу теорию о __cxa_begin_catch и __cxa_end_catch. Скомпилируем throw.cpp с флагом -S и проанализируем код ассемблера. Там есть много чего интересного, урежем до самого необходимого:
_Z5raisev:
call __cxa_allocate_exception
call __cxa_throw
Все идет замечателньо: мы получили такое же определение для raise (), лишь выброс искючения:
_Z18try_but_dont_catchv:
.cfi_startproc
.cfi_personality 0,__gxx_personality_v0
.cfi_lsda 0,.LLSDA1
Определение для try_but_dont_catch () обрезано компилятором. Это что-то новое: ссылка на __gxx_personality_v0 и что-то другое, называемое LSDA. Это выглядит незначительным определением, однако в действительности это очень важно:
- линкер использует это для CFI (call frame information) спецификации; CFI хранит информацию о фрейме вызова, вот его полная спецификация. Он используется, в основном, для раскручивания стэка.
- LDSA (language specific data area) — специальная для каждого языка область, используемая персональной функцией чтобы знать, какие исключения могут быть обработаны данной функцией.
О CFI и LSDA мы поговорим в следующей главе, не забывайте о них, но сейчас давайте двигаться дальше.
[...]
call _Z5raisev
jmp .L8
Еще одна элементарщина: просто вызываем raise
и после этого прыгаем на L8; L8 делаешь обычный возврат из функции. Если raise
выполнится неправильно, тогда выполнение (как-то, мы пока еще не знаем как!) не должно продолжится на следующей инструкции, а перейти к обработчику исключению (который в терминах ABI называется landing pads
, об этом позже).
cmpl $1, %edx
je .L5
.LEHB1:
call _Unwind_Resume
.LEHE1:
.L5:
call __cxa_begin_catch
call __cxa_end_catch
На первый взгляд этот кусок немного сложен, однако в действительности все просто. Наибольшее количество магии происходит тут: сначала мы проверяем — можем ли мы обрабатывать это исключение, если нет — вызываем _Unwind_Resume
, если можем — вызываем __cxa_begin_catch
и __cxa_end_catch
, после этого функция должна продолжится нормально и, таким образом, L8 будет выполнено (L8 прямо под нашим catch-блоком):
.L8:
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
Лишь обычный возврат функции… с некоторым мусором CFI в нем.
Это все для обработки ошибок, тем не менее мы до сих пор не знаем как работают __cxa_begin/end_catch, у нас есть идеи как эта пара формирует то, что называет landing pad — место в функции, где располагаются обраотчики исключений. Что мы пока не знает — как landing pads ищутся. Unwind должен как-то пройти все вызовы в стеке, проверить: имеет ли какой-либо вызов (фрейм стека для точности) валидный блок с landing pad, который может обрабатывать это исключение, и продолжить выполнение в нем.
Это немаловажное достичение, и как это работает мы выясним в следующей главе.
Ранее мы выяснили, что throw транслируется в пару __cxa_allocate_exception/throw, а catch-блок транслируется в __cxa_begin/end_catch, а так же во что-то, именнуемое CFI (call frame information) для поиска landing pads — точки входа обработчиков ошибок.
Что мы не знаем до сих пор, это как _Unwind узнает, где этот landing pads. Когда исключение пробрашивается сквозь связку функций в стэке, все CFI позволяют программе разворачивания стэка узнать, что сейчас за функция, а так же это необходимо, чтобы узнать какой из landing pads функции позволяет нам обрабатывать исключение (и, к слову, мы игнорируем функции с множественными try/catch блоками!).
Чтобы выяснить, где же этот landing pads находится, используется что-то, зовущее себя gcc_except_tale. Таблица эта может быть найдена (с мусором CFI) после конца функции:
.LFE1:
.globl __gxx_personality_v0
.section .gcc_except_table,"a",@progbits
[...]
.LLSDACSE1:
.long _ZTI14Fake_Exception
Эта секция .gcc_except_table — где хранится вся информация для обнаружения landing pads, мы поговорим об этом позже, когда будем анализировать персональную функцию. Пока что мы лишь скажем, что LSDA означает — зона с специфичными для языка данными, которые персональная функция проверяет на наличие landing pads для функции (она так же используется для запуска деструкторов в процессе разворачивания стэка).
Подытожим: для каждой функции, где есть по крайней мере один catch-блок, компилятор транслирует его в пару вызовов cxa_begin_catch/cxa_end_catch и, затем, персональная функция, вызываемая __cxa_throw, читает gcc_except_table для каждого мтеода в стэке для поиска чего-то, называемого LSDA. Персональная функция затем проверяет, есть ли в LSDA блок, обрабатывающий данное исклчение, а так же есть ли какой-то код очистки (который запускает деструкторы когда нужно).
Еще мы можем сделать интересный вывод: если мы используем nothrow (или пустой оператор throw), компилятор может опустить gcc_except_table для метода. Этот способ реализации исключений в gcc, не сильно влияющий на производительности, в серьезности сильно влияет на размер кода. Что касается catch-блоков? Если исключение пробрасывается когда nothrow определено, LSDA не генерируется и персональная функция не знает, что ей делать. Когда персональная функция не знает, что ей делать, она вызывает обработчик ошибок по-умолчанию, что, в большинстве случаев, означает, что выброс ошибки из nothrpw метода закончится std: terminate.
Теперь у нас есть идеи, что персональная функция делает, можем ли мы реализовать её? Что ж, посмотрим!
Продолжение