Обработка ошибок: Исключения

Обработка ошибок Исключения в c++ c

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

Числовые коды — это хорошо, и в некоторых случаях у вас не будет других вариантов, кроме как пользоваться ими. Однако механизм исключений во многом оказывается более удобным и гибким. Давайте разберемся, почему это так.

Почему исключения?

Вновь вернемся к нашему примеру функции регистрации пользователя из первой части. Но на этот раз она не будет возвращать код ошибки. Вместо этого она возбудит исключение, если что-то пойдет не так:

Для тех, кто не очень хорошо знаком с исключениями, напомню, что выбрасываем мы исключения с помощью throw . В качестве самого объекта-исключения может служить все, что угодно, даже примитивные типы, хотя лучше от этого подхода сразу отказаться и использовать классы. В данном случае мы использовали экземпляр класса RegistrationException . Равносильная запись выглядит следующим образом:

После возбуждения исключения нормальное выполнение функции прерывается. В типичной ситуации дальше мы либо попадем в обработчик исключения в этой же функции, который строится на основе блоков try-catch , либо начнем возвращаться по стеку вызовов функций, пока либо в одной из этих функций не встретим обработчик для нашего исключения, либо не дойдем до самого верха, включая функцию main() , после чего программа завершит работу с ошибкой, в которой будет указано о неперехваченном исключении.

Хорошо… Но лично я сразу вижу недостаток такой сигнатуры функции для registerUser() . По ней не видно, каким образом мы получим сообщение об ошибке. Однако следует признать, что в C++ есть возможность сообщить о том, что функция может выбросить исключение. Делается это следующим образом:

Проблема лишь в том, что синтаксис C++ не требует обязательного определения информации об исключениях, которые может возбудить функция. Более того, подобная конструкция не получила такого широкого распространения и используется не всегда и не всеми, чтобы можно было на него рассчитывать. В Java с этим намного лучше. Вы обязаны либо объявить о том, что метод может вернуть исключение, либо обеспечить его перехват в блоке try-catch . С другой стороны, в какой-то мере эту проблему решает хорошая документация, но она тоже есть не всегда. К тому же, стоит учитывать, что даже хорошая документация может вводить в заблуждение, поэтому старайтесь по возможности определять ограничения на уровне синтаксических структур самого языка, что упростит работу и вам, и тем, кто будет использовать ваши наработки.

Следующий вопрос, который у вас мог возникнуть: «А как организовать передачу информации о контексте, в котором произошла ошибка»? Когда мы использовали простые коды ошибок, у нас для этого было предусмотрено перечисление. Каждый элемент перечисления соответствовал тому или иному типу ошибки. Этот вариант широко распространен и неплохо работает, но он имеет свои ограничения. Например, с помощью одного лишь кода ошибки у нас нет возможности передать дополнительную информацию, чтобы уточнить причину ошибки. В случае же с исключениями у вас появляется выбор. Рассмотрим несколько наиболее очевидных вариантов.

Вариант 1. Коды ошибок. Опять

Мы ведь хотел уйти от кодов ошибок, а тут снова они? — В какой-то мере да. Однако на этот раз мы будем возвращать код не через явные выходные параметры функции, а с помощью экземпляра исключения:

В этом фрагменте нет ничего необычного. Единственный важный момент заключается в том, что класс исключения RegistrationException принимает код ошибки ErrorCode . Причем, обратите внимание, что мы убрали из перечисления код об успешном завершении работы функции. Ведь исключение может произой

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

Заметим однако, что в более сложных случаях мы все же можем получить некую выгоду:

Мы еще раз переписали функцию onUserDataReady() , с которой неоднократно экспериментировали в первой части. Она стала еще короче. А все за счет применения исключений. С другой стороны, теперь у нас всего один блок для обработки всех возможных ошибок, которые могли случиться. Однако если набор кодов ошибок продуман, то это не должно стать серьезной проблемой. Хотя логика обработки все равно будет не самой простой, поскольку может появиться блок switch или какие-то условные ветвления.

Вариант 2. Иерархия исключений

Если уж мы пишем ООП-код, то почему бы и ошибки не сделать объектами? Переход от кодов ошибок к иерархии классов осуществить не так уж сложно:

На первый взгляд преимущества не такие уж и большие. Просто теперь вместо case — ов у нас цепочка catch — ей, которые в целом равносильны. Однако это не совсем так. Ведь теперь у нас появляется возможность работы с контекстом, если мы предусмотрим передачу информации, которая представляет интерес, с помощью соответствующих объектов исключений. Например, в InvalidFieldValueException мы легко можем добавить имя поля, которое привело к ошибке:

Теперь мы не только знаем, какая ошибка произошла, но и что послужило причиной. Причем, поскольку исключение все равно остается полноценным классом, мы можем расширять его так, как посчитаем нужным, добавляя столько дополнительных сведений об ошибке, сколько потребуется. Кроме того, не забывайте про полиморфизм. Если вы добавите всего один обработчик для базового класса исключения, то он сработает и для всех его подтипов:

Вариант 3. Текстовые сообщения

Очень часто исключения предоставляют краткие сообщения о произошедшей ошибке. Если вы тоже собираетесь предусмотреть такую возможность, то имеет смысл организовать иерархию классов путем наследования одного из стандартных библиотечных классов исключений. Например, вы можете использовать для этой цели std:exception :

Важным моментом здесь является то, что деструктор нашего исключения и функцию-член what() мы были вынуждены объявить с явным указанием на то, что они не возбуждают никаких исключений. Это необходимо сделать по той причине, что именно с такой сигнатурой объявлены эти функции у базового класса std::exception , и не учесть это мы просто не можем, иначе получим ошибку компиляции. В самом же std::exception это сделано для того, чтобы дать гарантию на их безопасное использование в блоке catch , в противном случае мы вполне могли бы нарваться на еще одно исключение, а это было бы весьма неудобно.

В качестве текстового сообщения вы можете передать какую-то полезную информацию, чтобы затем в обработчике ее вывести:

В этом месте у вас мог возникнуть вопрос о том, как лучше компоновать подобные сообщения, насколько они должны быть детальными и т. д. Все зависит от ваших конкретных задач. Но следует учитывать, что эти сообщения предназначены в первую очередь для разработчиков. Например, если вы создаете приложение с графическим интерфейсом, то имеет смысл подготовить отдельный набор сообщений, которые вы будете показывать пользователям в случае ошибки, а те сообщения, которые будут передаваться в исключениях, выводите в log-файл или в стандартный поток ошибок std::cerr , как это показано в примере выше.

При этом в сообщениях, предназначенных для пользователей, не пытайтесь делать слишком подробные объяснения о технических деталях ошибки. Скорее всего, если пользователь увидит непонятный для него термин, то он просто не станет дочитывать сообщение. Хотя, конечно, ситуации бывают разные, поэтому в приложениях, рассчитанных на подготовленных специалистов, вполне допустимо использование технических подробностей об ошибках, вплоть до вывода внутренних сообщений, формируемых в исключениях. Приемлемым компромиссом в этом случае может стать реализация кнопки «Подробнее». То есть по умолчанию вы будете выводить лишь информацию о том, что что-то случилось, а все детали будут скрыты, но доступны для опытных пользователей по нажатию кнопки. Например, по этой схеме реализовано большинство сообщений об ошибках в Windows.

На самом деле, чтобы просто передать сообщение об ошибке, нам не требуется наследовать std::exception . С другой стороны, всегда лучше следовать неким стандартам, чтобы лишний раз не усложнять восприятие кода для тех, кому в будущем может потребоваться работать с ним. Приятным бонусом к этому является то, что если по какой-то причине исключение окажется неперехваченным, то после аварийного завершения приложения вы увидите не только название пропущенного исключения, но и текст сообщения, который будет выведен вызовом what() для него.

Коды ошибок, иерархия или текстовые сообщения?

Как обычно, все зависит от ваших целей. Интересным выходом может стать комбинированное использование всех трех вариантов. Например, мы можем реализовать класс исключения на случай некорректных значений полей с данными пользователя следующим образом:

Это лишь один из возможных подходов к созданию подобного класса исключения, и его можно улучшить. Но даже представленная реализация вполне неплохо справляется с комбинированием лучших качеств из рассмотренных нами вариантов использования исключений. В случае необходимости, чтобы возбудить исключение, мы передаем предопределенный код ошибки, которому соответствует шаблон текстового сообщения. Сами коды ошибок в этом случае мы определили внутри класса исключения, поскольку они непосредственно с ним связаны. Значения кодов ошибок в перечислении начинаются явным образом с 1000. Это поможет нам в дальнейшем не запутаться, если для других классов исключений мы будем определять коды в других диапазонах. Например, ошибки работы с БД мы можем определить, начав с 2000, а для сетевых соединений — с 3000.

Поскольку исключение InvalidFieldValueException сообщает о некорректном значении поля структуры, то в качестве дополнительной уточняющей информации мы передаем его значение. Название поля можно было передать тоже, но я решил, что текстового сообщения, сформированного на основании кода ошибки вполне достаточно. Обратите внимание, что для кода ошибки PASSWORD_IS_TOO_SHORT сам пароль, который был использован пользователем, в целях безопасности не выводится. С другой стороны, мы могли бы сделать некоторые уточнения в плане фактической длины полученного пароля, и допустимых диапазонах его значения.

Кроме того, следует заметить, что сообщение об ошибке мы сформировали в конструкторе, а не в функции what() . Это связано с тем, что оно строится динамически и не предопределено в виде статической константы. Если бы мы создали его внутри what() и попытались вернуть, то получили бы неопределенное поведение, поскольку выделенная память для сообщения автоматически освободится.

Коротко об освобождении ресурсов

Важным моментом при обработке исключений становится вопрос освобождения выделенных ресурсов. В предыдущей части, посвященной кодам ошибок, мы даже использовали goto для перехода к метке FINALLY , чтобы избежать дублирования кода. В некоторых языках программирования (например, Java или Python) по той же причине блок try-catch может быть расширен до try-catch-finally . Но в C++ на момент написания этой заметки такая возможность не предусмотрена. Конечно, вы можете добавить ее самостоятельно с помощью макросов, но есть гораздо более удобное решение. Оно называется RAII — Resource Acquisition Is Initialization (получение ресурса есть инициализация). Наверняка вы знакомы с умными указателями. Они и есть пример использования RAII. Суть этой техники довольно проста — необходимо создать класс-обертку, которая возьмет на себя управление ресурсом вплоть до его освобождения в своем деструкторе.

В первой части заметки мы использовали шаблонный класс AutoMemory , который обеспечивал автоматическое освобождение памяти. Но мы его не реализовали. Так давайте сделаем это:

Коды ошибок в си

// Не следует так делать…
if (timer. done)
// Одиночному оператору нужны скобки!
timer. control = TIMER_RESTART;

// А вот так правильно .
while (!timer. done)
<
// Даже пустой оператор должен быть окружён скобками.
>

ПРАВИЛО #2 – Ключевое слово «const»

Ключевое слово const следует использовать:

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

const unsigned char dirPort = 0xff;

ПРАВИЛО #3 – Ключевое слово «static»

static void InnerFunction( void )
<
….
>

ПРАВИЛО #4 – Ключевое слово «volatile»

Ключевое слово volatile нужно использовать везде, где уместно, включая:

— Объявление глобальной переменной доступной любому обработчику прерываний,
— Объявление глобальной переменной доступной двум или более задачам,
— Объявление указателя к отображаемым в памяти периферийным регистрам ввода/вывода

volatile unsigned int timer;

ПРАВИЛО #5 – Комментарии

// Так делать нельзя.
/*
a = a + 1;
/* comment */
b = b + 1;
*/

// Так правильно.
#if 0
a = a + 1;
/* comment */
b = b + 1;
#endif

ПРАВИЛО #6 – Тип данных с фиксированной разрядностью

ПРАВИЛО #7 – Поразрядные операторы

, ^, <<, и >>) не должен использоваться для управления целочисленными данными со знаком.

// Так делать нельзя.
int8_t signed_data = -4;
signed_data >>= 1; // не обязательно -2

ПРАВИЛО #8 – Целые числа со знаком и без

// Так делать нельзя.
uint8_t a = 6u;
int8_t b = -9;

if (a + b < 4)
<
// если бы -9 + 6 было -3 < 4 как ожидалось.
// была бы выполнена эта ветвь
>
else
<
//но поскольку -9 + 6 это (0x100 — 9) + 6 = 253
//то будет выполнена эта ветвь
>

ПРАВИЛО #9 – Параметризированные макросы против inline функций

Не следует использовать параметризированный макрос, если для выполнения той же задачи может быть написана inline функция. <2>

// Так делать нельзя.
#define MAX(A, B) ((A) > (B) ? (A) : (B))
// . если вместо этого вы можете сделать так.
inline int Max( int a, int b)

ПРАВИЛО #10 – Оператор-запятая

Оператор запятая (,) не должен использоваться внутри описания переменной.

// Так делать нельзя…
char * x, y; // вы хотите, чтобы «y» был указателем, или нет?

СНОСКИ

1 . Исходя из опыта консультирования множества компаний, я подозреваю, что подавляющее большинство встроенных систем содержит ошибки из-за нехватки ключевых слов volatile. Такие виды ошибок обычно обнаруживают себя как «глюки».
2 . Ключевое слово Си++ inline было добавлено в Си-стандарт в 1999-м году.

Michael Barr «Bug-Killing Coding Standard Rules for Embedded C». Вольный перевод ChipEnable. Ru

Comments

*ушел переписывать все параметризирова нные макросы в инлайн функции*

давно взял себе за правило всегда объявлять все переменные с фиксированной разрядностью (uint8_t, etc.)

Кто знает что есть такая книга :-) хорошо что есть такой сайт! Где можно узнать про книги и не только.

Не совсем корректный перевод. Скорее не «// Так делать нельзя. » а «// Так делать не рекомендуется.. .». Ибо Стандарт говорит, что все кроме примера из №5 синтаксически правильно.
С правилом №5 не согласен — комментарий, что /* */, что // написать и удалить в процессе отлаки в разы быстрее и удобнее, чем #ifdef. При помощи #ifdef имеет смысл комментрировать (заодно и выделяя таким образом) наброски нереализованных кусков в процессе написания, но не отключаемый в процессе отладки код. Аналогично не согласен со многими тезисами у Голуба.

Для правила №6 есть дополнение — существуют еще типы uint_leastXX_t и uint_fastXX_t, про которые тоже неплохо бы написать. Например, счетчик цикла от 0 до 10 имеет смысл объявлять не int, а uint_fast8_t, который будет 8-битным беззнаковым для AVR и 32-битным беззнаковым для ARM.

Источники:

https://tech-geek. ru/error-handling-exceptions-in-c/

https://chipenable. ru/index. php/item/62

Понравилась статья? Поделиться с друзьями:
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: