
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
- Сконструированный объект пробрасывается обратно по стэку
- До встречи подходящего блока
try
\catch
(если блок не найден, то аварийное завершение) - “Раскручивая” стэк обратно уничтожаются все объекты с automatic storage duration (!Nota benne Если исключение не перехватывается, то stack unwinding зависит от реализации)
std::terminate
если в процессе возникает еще одно исключение (функция для принудительного завершения программы. Она вызывается, когда в программе выброшено не перехваченное исключение)- Все деструкторы
noexcept
(иначе во время разворачивания стека будет несколько исключений) - ! Сам объект хранится в неопределенном участке памяти
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
)