Все мы знаем, а многие даже читали, книгу Герба Саттера "Решение сложных задач на С++", которая появилась благодаря известным публикациям из серии под названием "Guru of the Week". Одна из задач в этой книге была посвящена std::uncaught_exception и звучала она так:

GotW #47: "Что собой представляет стандартная функция std::uncaught_exception и когда она должна использоваться?" 

Ответ дан в книге Саттера еще в далеком 2002 году. Однако, комитет решил вернуться к обсуждению этой "редкой" функции в 2013. Что же решили изменить и для чего все-таки нужна эта функция, я постараюсь "за 5 минут" рассказать в данном посте. Уделите время, и Вы также узнаете про декларативный подход в обработке ошибок, предложенный Александреску.

Бесполезная функция

Стандартная функция std::uncaught_exception() позволяет понять, не является ли в настоящее время активным какое-то исключение. Эта функция возвращает булевое значение, которое равно true в том случае, если в момент вызова std::uncaught_exception() происходит раскрутка стека в данном потоке.

Как часто она применяется на практике? Скорее всего очень редко, возможно, что и никогда. В своей книге Саттер, после нескольких показательных примеров, говорит следующее:

"К сожалению, я не знаю ни одного полезного и безопасного применения функции std::uncaught_exception(). Мой совет: не используйте её!" GotW #47

Казалось бы, что после таких слов про эту функцию можно было забыть навсегда. Однако, в 2013 году Саттер вновь возвращается к ней, и даже пишет предложение в комитет под номером N3614 (и уточнение N4152), в котором предлагается эту функцию заменить... Очень показательно то, что сам Саттер "не забывал" про std::uncaught_exception(), хотя и советовал не использовать  эту функцию. Зачем же снова обсуждать её, и пытаться что-то улучшить в новом стандарте С++17?

ScopeGuard

Функция std::uncaught_exception снова попала в поле зрения во многом благодаря Андрею Александреску. Саттер пишет, что именно его примеры послужили главным мотивом к пересмотру возможностей uncaught_exception. Речь идет о реализации класса ScopeGuard, который предложил Александреску. Вот ссылка на его лекцию, которая называется "Error Handling in C++". Она довольно большая и подробная, я же постараюсь "в двух словах" рассказать, зачем нужен ScopeGuard. 

ScopeGuard - это шаблон класс, который позволяет выполнить какие-либо действия в рамках определенной области, в том случае если, в данной области произошло исключение, и в случае, если исключения не произошло. Если Вы знакомы с языком Python или Java, то знаете про оператор finally, который позволяет выполнить блок инструкций в любом случае, было ли исключение, или нет. Язык С++ не поддерживает finally, однако у нас есть идиома RAII. Деструктор локальной переменной будет вызван при выходе её из области видимости, и в случае возникновения исключения. А значит мы сможем сделать те действия, которые "поместили" бы в блок finally, будь он в нашем арсенале. Но всё не так просто...

Что если действия, которые требуется выполнить, различны для случая, когда было возбуждено исключение, и для случая "нормального" выполнения кода (без исключений). Например, нам требуется реализовать "rollback" в случае возникновения исключения, то есть откат всех изменений, внесенных с определенной точки. Вот тут-то нам и помогла бы uncaught_exception:

Всё замечательно и просто, за исключением того, что... этот код не будет правильно работать вот в каком случае:

Во время выполнения программы возникло исключение ( throw 1 ). Ничего страшного в этом нет, и мы готовы его обработать. Но перед этим, в процессе раскрутки стека, будут вызваны деструкторы локальных объектов, в том числе и foo. В деструкторе foo вызывается функция LogStuff, чтобы всю информацию внести в базу данных. Для этого создается объект класса Transaction, и выполняются все необходимые действия.  Обратите внимание, что функция LogStuff отрабатывает без ошибок, а значит данные должны попасть в базу. Но! Когда вызывается деструктор объекта Transaction (по выходу из функции LogStuff), uncaught_exception() вернет true. Так как мы находимся в процессе раскрутки стека, и есть активное исключение. И все наши данные лога будут "откатаны" и потеряны. И хотя проблем с LogStuff никаких не случилось, наша транзакция среагировала на "внешнее" исключение, в то время когда, нас интересовали только исключения с момента создания объекта Transaction и до момента его уничтожения. Другими словами, важно не то - было ли исключение вообще, а то - было ли исключение в определенной области видимости. Именно этот пример приводит Саттер в своем письме комитету.

Поэтому ScopeGuard нельзя написать используя uncaught_exception. Ведь уже название подчеркивает, что Guard "охраняет" определенный "Scope", а не распространяется на всю программу.

Александреску

В настоящее время в стандартной библиотеке С++ (включая стандарт С++14) нет ничего, что помогло бы отличить успешное выполнение кода в определенной области видимости, от ситуации, когда именно в этой области возникло исключение.

Однако, Андрей Александреску придумал, как реализовать ScopeGuard: он предложил ввести функцию int getUncaughtExceptionCount(), которая возвращала бы сколько исключений активно в данный момент (т.е. возбуждено, но не обработано). Если мы будем располагать такой информацией, то нам удастся реализовать настоящий ScopeGuard. Вот решение от Александреску:

Александреску указал, что реализовать функцию int getUncaughtExceptionCount() сегодня возможно на всех основных компиляторах С++. А Саттер, помня о схожей (но не удачной) функции uncaught_exception, предложил переименовать getUncaughtExceptionCount() в std::uncaught_exceptions, просто добавив в конце "s". И вот результат:

http://en.cppreference.com/w/cpp/error/uncaught_exception

В С++17 появится функция std::uncaught_exceptions, в замен "старой" std::uncaught_exception. Эта функция позволит реализовывать классы вида ScopeGuard, которые в свою очередь позволят обрабатывать ошибки в декларативном виде, не используя явно try/catch во многих ситуациях. Примеры такого подхода Вы сможете найти в N4152. В конце этого документа приложена презентация Андрея Александреску. Изучая примеры использования, начинаешь понимать, что нам может дать std::uncaught_exceptions. Вот самый простой случай:

Примеров здесь можно привести очень много, например, с вложенными блоками try/catch, которые можно переписать с использованием ScopeGuard в намного более понятной форме. И всё это, есть продолжение идиомы RAII, которую все мы очень любим. Поэтому комитет принял предложение ввести uncaught_exceptions, взамен "старой" uncaught_exception. Благодаря стараниям Саттера и Александреску.

Вот такая короткая история про "бесполезную" функцию uncaught_exception :)


C++ развивается несмотря ни на что, а мы с интересом следим за этим процессом. Спасибо Вам за внимание!

P. S.: Все примеры любезно предоставлены Гербом Саттером и Андреем Александреску. Спасибо им за это.