– Сир, я придумал защиту от дракона. Он нам больше не страшен! Она срабатывает от взмахов крыльев дракона и включает громкую сирену, так чтобы все слышали, что приближается дракон.
– Что-нибудь ещё эта защита делает?
– Нет, зачем? Мы будем предупреждены!
– Да… Съедены под вой сирены… И ещё… напомни, когда у нас плановые отключения электричества?…
Данный способ не претендует на концепцию обработки ошибок в комплексных и сложных проектах. Скорее это пример того, что можно сделать минимальными средствами.
Хорошая норма – считать, что в ходе выполнения программы не должен срабатывать ни один 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 версии).
Для всех макросов важно:
Конечно же, самое интересное, супермены энтропии (герои уменьшения ошибок в программах), это использование этих макросов.
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) или аналогичной. Например так:
Тогда можно написать следующий код:
Конечно, это только одна из возможностей реализации. Можно и другими способами реализовать макросы.
P.S. Успешного сражения с энтропией, супермены!
– Что-нибудь ещё эта защита делает?
– Нет, зачем? Мы будем предупреждены!
– Да… Съедены под вой сирены… И ещё… напомни, когда у нас плановые отключения электричества?…
Описание проблемы
Данный способ не претендует на концепцию обработки ошибок в комплексных и сложных проектах. Скорее это пример того, что можно сделать минимальными средствами.
Хорошая норма – считать, что в ходе выполнения программы не должен срабатывать ни один 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. Успешного сражения с энтропией, супермены!