Error Handling

среда, мар. 5, 2025 | 5 минут чтения

Error Handling

2 семестр
Основы программирования.

Какие могут возникать ошибки при написание программы (делаем что-то некорректное, но не потому что вы крабик, а потому что сервер мог не дать):

  • Выход за границу массива
  • Деление на ноль
  • Невозможность выделить память
  • Отсутствие прав на открытие файла
  • Недоступность внешнего сервера

Варианты обработки

  • Обычный assert (runtime)
    • если проверка не истинна, то программа просто падает
#include <cassert>

int main() {
	assert(2+2 == 4)
	assert(2+2 ==5)
	return 0;
}
// Assertion '2+2==5' failed.
  • Static assert (compile time)
    • помогает выяснить разные факты про разные типы
static_assert(sizeof(int) == 4, "int must be 4 bytes" );

template <typename T>
struct data_structure {
	static_assert(
		std::is_default_constructible< T>::value,
		"Data Structure requires default-constructible elements"
	);
};
struct no_default {
	no_default () = delete;
};
int main() {
	data_structure<no_default> ds_error;
	return 0;
}

Код возврата

Есть разные методы получить какое-то число/значение ошибки, а потом перебирать разные варианты и делать разные действия в зависимости от значений:

  • return (int) ex. количество успешно записанных байт в поток size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );

  • [[https://en.cppreference.com/w/c/error/errno|errno]] (error number, глобальный макрос, в который можно записать код ошибки, а после перевести в текстовый вид)

    • ошибок очень много, сложно все учесть
    • есть только один индикатор, который смотрит на ошибки (потому что это глобальная переменная), поэтому при следующей ошибке прошлая затрётся и мы не будем знать о ней FILE *fopen( const char *filename, const char *mode ); fopen возвращает указатель на файл(файловый дескриптор), а если его не открыть (нет прав, может не существовать, используется в другой программе), то nullptr.
  • ошибка в качестве кода возврата

errno_t fopen_s(FILE *restrict *restrict streamptr, const char *restrict filename, const char *restrict mode); при неоткрытии файла вернётся циферка, отвечающая за код ошибки

Обработка в месте возврата

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

Exception. throw, try, catch

int foo() {
	throw std::runtime_error("error");
}

void boo() {
	throw 2;
}

void coo() {
	throw std::string("Hello world");
}

int main(int, char**) {
	try{
		foo();
	}
	catch(...) {
		//do something...
	}
}
  • throw - сообщение о происшествии исключительной ситуации (сообщаем, что что-то пошло не так)
  • try - в ниже лежащем коде пытаемся найти ошибки из throw
    • после try может находиться любой объект
  • catch обработка ошибок, найденных в try
  • в тот момент, когда вызывается throw,начинает работать механизм stack unwinding Stack unwinding
  1. Сконструированный объект пробрасывается обратно по стэку
  2. До встречи подходящего блока try\catch (если блок не найден, то аварийное завершение)
  3. “Раскручивая” стэк обратно уничтожаются все объекты с automatic storage duration (!Nota benne Если исключение не перехватывается, то stack unwinding зависит от реализации)
  4. std::terminate если в процессе возникает еще одно исключение (функция для принудительного завершения программы. Она вызывается, когда в программе выброшено не перехваченное исключение)
  5. Все деструкторы noexcept (иначе во время разворачивания стека будет несколько исключений)
  6. ! Сам объект хранится в неопределенном участке памяти
struct Foo {
	Foo() { std::cout << "Foo()\n"; }
	~Foo() {std::cout << "~Foo()\n";}
};

void internalFunc() {
	Foo f;
	throw std::runtime_error("Some error");
}

void externalFunc() {
	try {internalFunc();}
	catch (const std::exception& e) {std::cout << e.what() << std::endl;}
}
Foo()
~Foo()
Some error

Объяснение:

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

Можно обрабатывать где-то в другом месте, а не в месте возникновения. Главное где-то по стеку.

Можно кидать несколько throw, а обрабатывать единожды

  • Нужно использовать идиому RAII, чтобы не было утечки памяти (когда всё снимается со стека, вызываются деструкторы и нет утечек)
void internalFunc() {
	Foo* f = new Foo;
	throw std::runtime_error("Some error");
	delete f;
}

catch-блоков может быть много, таким образом мы можем обрабатывать разные ошибки по разному

int main() {
	try {
		foo();
	} catch (const std::overflow_error& e) {
		// do something
	} catch (const std::runtime_error& e) {
		// do something
	} catch (const std::exception& e) {
		// do something
	} catch (...) {
		// do something
	}
}

Ищем первый catch-блок, который нам подходит (тип, который мы пробросили приводим к типу, который мы ловим - в том числе и классы наследники к базовым (не забываем!)). Гарантированно попадаем только в один catch-блок.

Если нет catch блока, который подходит под тип брошенного исключения, то будет terminating due to uncaught exception

Если подходящих catch-блоков несколько, то берётся самый первый Нужно следить за последовательностью catch-блоков (указываем от самого нижнего класса, до самого базового) Если хотим обрабатывать вообще всевозможные ошибки, то ставим catch(...) - точнее сказать, используется вообще для любого объекта - не знаем, что именно случилось - очень плохо - но можно использовать в качестве else, если все catch-блоки до этого не сработали Передача в catch аналогично передаче в функции (поэтому стандарт const ref, чтобы не было копий)

Гарантии безопасности исключений

  • No guarantee

    • вообще никаких гарантий о том, что наш класс остался в инвариантном состоянии
  • Basic guarantee

    • Сохраняется инвариант
    • Нет утечек памяти
  • Strong guarantee

    • Сохраняется инвариант
    • нет утечек
    • Состояние возвращается к состоянию до исключения
  • Nothrow guarantee

    • Не может быть выкинуто исключение
  • тут в примере утечка из-за оператора присваивания…

    • при конструкторе копирования вызвано исключение, соответственно никакой объект не сконструировался (basic, потому что nullptr для нас инвариант)
    • чтобы сделать сильную гарантию
      • сделать копию объекта
      • сделать временный объект, который мы и будем пробовать конструировать, а потом свапнуть
        • Copy And Swap idiom
          • если не сконструировался, то исключение
          • реализовать свою функцию своп для хорошего конструирования

Ключевое слово noexcept - гарантия, что данный метод не умеет кидать исключения

  • Гарантирует что функция не будет бросать исключения

  • Не сворачивает стэк

  • Позволяет компилятору лучше оптимизировать код

  • std::terminate

  • Деструктор noexcept по умолчанию (потому что при сворачивание стека может выброситься ещё одно исключение и будет std terminate )

  • чтобы компилятор не генерил ассемблер для обработки исключений Все stl-контейнеры имеют гарантии на свои методы (чаще всего строгие)

    P.S. когда объявление и инициализация - оператор присваивания (boo b1 = b), когда без - то копирования(b1 = b)