--- title: Exceptions tags: cpp, mipt, 3sem, teaching, exceptions --- # Восьмое занятие #### 2022-10-22 ## Исключения ### Основная информация Способ контроля работы программы. Пример: выделение динамимческой памяти. Оператор `new` может вызвать (или же *выбросить*) исключение [`std::bad_alloc`](https://en.cppreference.com/w/cpp/memory/new/bad_alloc). Исключение свидетельствует о том, что не удалось выделить достаточное количество памяти. В таком случае возникает желание уменьшить количество выделяемой памяти. Делается это следующим образом: ```cpp= #include <iostream> int main() { double* ptr; try { ptr = new double[20000000]; } catch (std::bad_alloc const& err) { std::cout << err.what() << '\n'; ptr = new double[20000]; } catch (...) { std::cout << "Something very bad!\n" << std::endl; } delete [] ptr; return 0; } ``` В данном примере мы увидели использование `try-catch`-блока. В `try` оборачивается блок, который потенциально может вызвать ошибку. В следующем блоке попытка обработать данную ошибку. И последний блок ловит ошибку в любом случае, что означает очень печальное состояние программы. Еще один пример: деление на ноль. ```cpp= #include <iostream> #include <string> double inv(double x) { if (x == 0) { throw std::string("Exception!"); } return 1. / x; } int main() { inv(0.0); return 0; } ``` В момент попадения на строку 6 начинается уничтожение всех локальных переменных, а потом постепенное разворачивание стека вызова до момента попадения в `main`. После этого программа сообщает операционной система о результате работы и все завершается аварийно (аварийное завершение). Как видно по сигнатуре `throw` возможен выброс **любого** объекта. Соответственно, в блоке `catch` можно ловить объекты любого типа (почти). В блоке `catch` можно ловить объект с копированием, по ссылке и даже по указателю. `dynamic_cast<T>` -- еще один из стандартных операторов, который кидает исключение. ### Что НЕ есть исключение Исключением являеется все то, что описано в стандарте как исключение - Обращение к невыделенной памяти -- не исключение (это undefined behavior) - Выход за границы массива -- не исключение (это undefined behavior) - Неправильное разыменование указателя -- не исключение Исключение можно обработать, непредвиденные ситуации решению не подлежат (пока). ### Обработка ошибок Проведем проверку работы `throw`. ```cpp= #include <iostream> struct S() { S() { std::cout << "S()\n"; } S(S const&) { std::cout << "S(S const&)\n"; } S& operator=(S const&) { std::cout << "operator=(S const&)\n"; } ~S() { std::cout << "~S()\n"; } } void f() { S s; throw s; } int main() { try { f(); } catch(S exc) { std::cout << "cought S in main\n"; } return 0; } ``` Получается, что при выкидывании ошибки происходит копирование объекта. Если ошибка ловится по значению, то она снова копируется. В блоке `catch` можно провести очередной *бросок* ошибки. И снова повторится процедура копирования. Это можно избежать, если ловить по константной ссылке. ### Приведение типов При выбрасывани объектов они ловятся строго в соотвествии с их типом. Не происходит неявного преобразования. Но есть исключение в данном случае. Если выбрасывается объект `Derived`, который является наследником `Base`, то можно словить объект по ссылке на базовый класс. Стоит отметить, что не стоит ловить производные классы после базовых. Это просто не сработает. ### Исключение в конструкторах ```cpp= struct MyArray { size_t size = 0; int* buffer; int* buffer_add; MyArray() { size = 5; buffer = new int[5]; throw std::string("Error"); buffer_add = new int[5]; } ~MyVector() { delete [] buffer; delete [] buffer_add; } } ``` ### Исключение в деструкторах Исключение в деструкторах запрещены. Но если сильно захотеть, то можно ### noexcept Чтобы указать на неисключительность вашего метода (убедить компилятор, что вы не будете выкидывать исключение из данного метода), существует модификатор `noecxept`. ```cpp= #include <iostream> #include <vector> class MyClass { public: MyClass(int n, float f): size(n), ptr(new float[size]) { for (int i = 0; i < n; ++i) { ptr[i] = f; } std::cout << "MyClass()\n"; } MyClass(const MyClass& other): size(other.size), ptr(new float[size]) { for (int i = 0; i < size; ++i) { ptr[i] = other.ptr[i]; } std::cout << "MyClass(const MyClass& other)\n"; } MyClass(MyClass&& other) { if (this == &other) { return; } size = other.size; ptr = other.ptr; other.size = 0; other.ptr = nullptr; std::cout << "MyClass(const MyClass&& other)\n"; } ~MyClass() { std::cout << "~MyClass()\n"; } private: int size; float* ptr; }; int main() { std::vector<MyClass> vec; std::cout << "Push Back\n"; vec.push_back(MyClass(12, 3.1415f)); std::cout << "Emplace Back\n"; vec.emplace_back(1, 2.71828f); std::cout << "final point of program\n"; return 0; } ``` ## Задание 1. Реализуйте свое исключение. Вам потребуется базовый класс для исключения `base_exception` и отнаследуйтесь от него для создания двух видов ошибок: `math_exception` и `invalid_argument`. Первое исключение вызывается при делении на ноль, второе -- при получении отрицательного значения при подсчете корня квадратного. Дожен быть реализован метод `what()`. Провести проверку работы данных исключений ```cpp= #include <iostream> #include <cmath> struct base_exception { // smth here }; struct math_exception final: base_exception { // smth here }; struct invalid_argument final: base_exception { // smth here }; double inverse(double x) { if (x == 0) throw math_exception(); return 1. / x; } double root(double x) { if (x < 0) throw invalid_argument(); return std::sqrt(x); } ``` 2. Реализуйте вектор по следующему шаблону: ```cpp= template <typename T> class Vector { public: Vector(); Vector(Vector const& vec); Vector(Vector&& vec) noexcept; void push_back(const T& value); T& top(); // Безопасное получение ссылки на верхний элемент; // Если его нет, то исключение void pop(); // Безопасное извлечение (удаление) верхнего элемента; // Если его нет, то исключение void is_empty() const; void capacity() const; T& at(size_t index); // Безопасное получение ссылки на i-ый элемент; // Если его нет, то исключение T& operator[](size_t index); // Получение ссылки на i-ый элемент const T& operator[](size_t index) const; // Получение const ссылки на i-ый элемент private: size_t size = 0; T* buffer = nullptr; void add_memory(); ~Vector(); } ``` 3. Попробовать разделить вектор на два класса: - `Buffer` -- предоставляет механизм работы с памятью - `Vector` -- интерфейс, который является оберткой над `Buffer`. Самостоятельно лишь описывает правила работы с `Buffer`, но не лезет в память напрямую. ```cpp= template <typename T> class Buffer { private: T* buffer; public: Buffer(); T& top(); private: void add_memory(); T& operator[](size_t index;) const T& operator[](size_t index); } ```