Pull to refresh

Расширение макроса assert() для реализации минимальной обработки ошибок

Reading time5 min
Views2.9K
– Сир, я придумал защиту от дракона. Он нам больше не страшен! Она срабатывает от взмахов крыльев дракона и включает громкую сирену, так чтобы все слышали, что приближается дракон.
– Что-нибудь ещё эта защита делает?
– Нет, зачем? Мы будем предупреждены!
– Да… Съедены под вой сирены… И ещё… напомни, когда у нас плановые отключения электричества?…

Описание проблемы


Данный способ не претендует на концепцию обработки ошибок в комплексных и сложных проектах. Скорее это пример того, что можно сделать минимальными средствами.

Хорошая норма – считать, что в ходе выполнения программы не должен срабатывать ни один assert(). А если сработал хотя бы один assert() при тестировании приложения, то нужно отправить эту ошибку разработчику. Но, что если приложение не будет протестировано полностью? И assert() сработает у клиента? Отправить ошибку разработчику? Прервать выполнение программы? В реальности это будет release версия приложения и стандартный assert() просто будет отключен. Также возникает вопрос с внутренним противоречием системы: assert()-ов должно быть много, что бы было легче обнаружить ошибки, но assert()-ов должно быть меньше, чтобы меньше прерывать пользователя и его работу с приложением. Особенно не хотелось бы «падать», если от стабильности работы зависит то, сколько человек использует приложение и если assert() по сути был незначительным (требующим исправления, но позволявшим, например, вполне успешно продолжить работу).

Такие размышления приводят к необходимости доработать assert() c/c++. И определить свои макросы, которые расширяют функциональность стандартного assert()-а путем добавления минимальной обработки ошибок. Пусть такими макросами будут.

VERIFY_EXIT(Condition);
VERIFY_RETURN(Condition, ReturnValue);
VERIFY_THROW(Condition, Exception);
VERIFY_DO(Condition) {/*fail block*/};


(Эти макросы можно назвать и по другому. Например, VERIFY_OR_EXIT(), VERIFY_OR_RETURN(), VERIFY_OR_THROW(), VERIFY_OR_DO(). Или наоборот в более сокращенном варианте.)

Эти макросы, во-первых, имеют реализацию как для debug версии компиляции так и для release версии. Что позволяет им иметь поведение и в release версии программы. Т.е. выполнять действия не только при тестировании, но и у пользователя.

Описание макросов


(Описание макросов примерное, возможен и другой их дизайн.)

1) VERIFY_EXIT(Condition);

Проверяет условие Condition и если оно false, то вызывает стандартный assert() (debug версия), а также выходит из текущей функции (debug и release версии).

2) VERIFY_RETURN(Condition, ReturnValue);

Проверяет условие Condition и если оно false, то вызывает стандартный assert() (debug версия), а также выходит из текущей функции возвращая значение ReturnValue (debug и release версии).

3) VERIFY_THROW(Condition, Exception);

Проверяет условие Condition и если оно false, то вызывает стандартный assert() (debug версия), а также бросает исключение Exception (debug и release версии).

4) VERIFY_DO(Condition) {/*fail block*/};

Проверяет условие Condition и если оно false, то вызывает стандартный assert() (debug версия), а также выполняет блок операций (fail block) или операцию сразу следующий за макросом (debug и release версии).

Для всех макросов важно:

  • Во всех случаях Condition должен быть истинным для «прохождения» макроса и ложным для активации пути минимальной обработки ошибки.
  • Каждый из макросов реализует некоторый минимальный способ обработки ошибки. Это необходимо для реализации поведения в случае ошибок, которые не были обнаружены при тестировании, но произошли у пользователя. В зависимости от реализации, можно сообщать разработчику о произошедшей у клиента ошибке, но также каждая реализация дает минимальный способ восстановиться при ошибке.

Паттерны использования макросов


Конечно же, самое интересное, супермены энтропии (герои уменьшения ошибок в программах), это использование этих макросов.

1) Pre и post условия.

Первый вариант использования это pre и post условия. Напомню, что pre условия проверяют состояние программы (входные аргументы, состояние объекта, используемые переменные) на соответствие необходимым требованиям выполняемого фрагмента кода. Post условия (они реже встречаются в программах) предназначены для проверки того, что мы достигли необходимого результата и состояние объектов осталось валидным для текущего фрагмента кода.

Использование предлагаемых макросов прямолинейное – каждую проверку мы прописываем в отдельном макросе. Макросы мы выбираем исходя из того, какая обработка ошибок нам требуется. (VERIFY_EXIT() – обработка ошибки с выходом из данной функции, VERIFY_RETURN() – обработка ошибки с возвратом некоторого значения, VERRIFY_THROW() – обработка ошибки с генерацией исключения и т.д.)

Также можно добавить или использовать макрос VERIFY(), который не будет совершать никакой обработки ошибки. Это может быть полезным, например в post условия в конце функции.

Данные макросы вполне самодостаточны, если вы используете принципы чистого кода и выделяете достаточно количество функций для реализации атомарных действий. Каждая функция может проверять состояние объекта, входные аргументы и т.д. для выполнения своего атомарного действия.

2) Семантика транзакции.

Также эти макросы могут быть использованы для реализации кода с семантикой транзакции. Под такой семантикой понимается: 1) постепенная подготовка к выполнению операции с проверкой результатов каждого из этапов подготовки; 2) выполнение действия только если все этапы подготовки прошли успешно; 3) отказ от выполнения, если некоторые условия не соблюдены на этапе подготовки (с возможным откатом от выполнения).

3) Проектирование кода с учетом возможного расширения.

Это особенно актуально для библиотек и общего кода, который первоначально может разрабатываться в рамках одного контекста условий выполнения, а позже может начать использоваться с другими условиями (начать использоваться иначе). В таком случае данные макросы могут описать «границы» функциональности кода. Определить, что первоначально рассматривалось как ошибка, а что являлось успешным выполнением. (Этот подход близок к классическим pre post условиям.) Конечно, «границы» я пишу в кавычках, т.к. эти границы могут быть пересмотрены, но важно определить (а точнее передать будущим разработчикам) знание о допустимых границах проектирования кода.

Реализация макросов


Я предполагаю у большинства разработчиков уже среднего уровня не вызовет проблем реализация этих макросов. Но если нужна информация, то уделю некоторым важным моментам.

Макросы должны быть представимы в виде одного оператора. Что можно сделать с помощью конструкций do{}while(false) или аналогичной. Например так:

#define VERFY_EXIT(cond)	\
do{bool _= (bool)(cond); assert(_); if(!_) {return;}} while(false)	\
/*end macro VERIFY_EXIT()*/

Тогда можно написать следующий код:

if(a > 0) VERIFY_EXIT(a%2==0);

Конечно, это только одна из возможностей реализации. Можно и другими способами реализовать макросы.

P.S. Успешного сражения с энтропией, супермены!
Tags:
Hubs:
Total votes 13: ↑10 and ↓3+7
Comments16

Articles