[БЕЗ_ЗВУКА] В данном видео мы поговорим об исключениях в языке C++.
Мы разберемся, зачем нужны исключения, как реализованы исключения в языке C++,
как их надо выкидывать и что с ними вообще делать,
а также узнаем, как обрабатывать исключения в вашем коде.
Если кратко, то исключение — это нестандартная ситуация, то есть,
когда ваш код ожидает, что вокруг него будет какая-то определенная среда или
определенные инварианты, а они не соблюдаются.
Самый банальный пример — у вас есть функция, которая суммирует две матрицы, и,
например, одна из матриц имеет неправильную размерность.
Все. Исключительная ситуация —
здесь можно бросить исключение.
Как это сделать — разберемся прямо сейчас.
В данной лекции мы поговорим о том, как обрабатывать ошибки в языке C++.
Давайте рассмотрим следующий пример,
когда у нас есть класс Date, и мы ее хотим как-то парсить с входного потока.
Ну окей.
Что у нас есть в дате?
В дате есть год, в дате есть месяц и в дате есть число.
Замечательно, классик мы создали.
Что мы хотим?
Ну, давайте захотим, например, читать дату с входного потока или давайте,
чтобы было совсем просто, будем читать дату из строки.
Заведем переменную date_str и запишем
туда следующую дату: 2017/01/25,
это будет 25 января 2017 года.
Отлично.
Давайте заведем функцию ParseDate, которая нам будет возвращать дату,
на вход она будет принимать строку.
[ЗВУК] На вход
она будет принимать константную строчку, в которой будет храниться текст даты.
Значит, что мы здесь дальше будем делать?
Мы объявим строковый поток, с которым дальше будем работать.
Назовем его stream и положим туда входную строчку.
После этого заведем переменную даты
date и начнем из этого потока считывать туда всю необходимую информацию.
То есть считаем год,
после этого нам надо пропустить следующий символ.
Как мы помним, там идет слэш, то есть 2017,
вот мы его считали в строчке 18, далее идет слэш, потом 01, потом слэш, потом 25.
Слэш мы можем пропустить с помощью функции ignore.
Сюда надо передать количество символов, которое мы хотим пропустить.
После этого аналогичным образом мы считаем месяц и день.
То есть еще раз: считали год, пропустили слэш,
считали месяц, пропустили слэш, считали день.
Отлично.
Наша дата считана, мы можем вернуть нашу дату.
Давайте выведем то, что у нас получится, на экран.
Ну, для начала объявим переменную типа Date,
назовем ее date и распарсим строчку выше.
После этого с помощью
cout мы сможем красиво вывести эту дату.
В данном случае я устанавливаю ширину поля 2 и заполнитель 0,
чтобы когда мы вводим числа меньше 10, у нас был красивый заполнитель в виде 0,
и дата смотрелась изящно.
Выведем день,
поставим точку, после этого...
Ну, на самом деле, мы можем повторить весь этот код и вывести месяц.
[ЗВУК] После
этого мы опять можем
немного повторить наш код,
точнее даже просто вывести год.
И не забываем перенос строки.
Что ж, запустим наш код.
Отлично.
Видим, что дата распарсилась правильно: 25.01.2017.
Вроде задача решена,
и как-то эта функция у нас работает, но давайте попробуем защититься от ситуации,
когда данные на вход могут прийти не в том формате,
совсем-совсем не в том, который мы ожидаем.
В данном случае мы явно задекларировали, когда ставили задачу,
что разделитель — это слэш.
Там может быть, например, не знаю, буква a, тут может быть буква b,
и это уже не та дата, которую нам надо распарсить.
То есть непонятно вообще, дата ли это или это какой-то специальный код.
Хорошо бы в таком случае донести до того, кто вызывает эту функцию, о том,
что формат входных данных, он неправилен.
Но сейчас функция этого совершенно не делает.
То есть мы запустим данный код, он скомпилируется, и из этой какой-то
странной строчки, которая совсем не похожа на дату, мы извлекли 25 января 2017 года.
Почему так получается?
Потому что мы считаем сначала 2017 в переменную «год», после этого
пропустим букву a, после этого считаем месяц, пропустим букву b и считаем день.
Как от этой ситуации можно защититься?
У потока есть метод peek.
Он позволяет посмотреть, какой следующий символ идет в потоке.
С одной стороны, мы можем написать много проверок на то,
что если функция peek возвращает не слэш, то все плохо, надо вернуть false,
тогда у нас еще и, замечу, поменяется сразу сигнатура функции.
ParseDate должна будет возвращать bool и, видимо, отдельным аргументом она
должна будет принимать дату, которую мы будем модифицировать.
И вот этот флажок bool, он будет означать,
удалось нам дату распарсить или не удалось.
При этом так надо будет делать вообще во всех местах,
где мы используем эту функцию.
Это немного утомительно, это усложняет наш код,
и поэтому я даже писать не буду эту реализацию.
Давайте рассмотрим то, что есть в языке C++ специально для отлова таких ситуаций.
Это исключения.
Исключение — это специальный механизм,
который позволяет сообщить вызывающему коду,
что произошла какая-то ошибка, что-то пошло не так, что-то пошло не по плану.
Мы ожидали одни данные, а получили другие.
Мы ожидали, что интернет у нас есть, а интернета у нас нет.
В общем, это непредвиденная ситуация, о которой мы хотим явно сообщать.
Давайте просто рассмотрим пример, и дальше станет,
я надеюсь, все намного понятнее.
Во-первых, давайте, ну давайте сначала просто будем считывать
наши символы из потока и сообщать об этих исключительных ситуациях.
То есть напишем if
(stream.peek() != '/',
то воспользуемся специальным синтаксисом.
throw, а дальше выбрасываем специальный класс в языке C++ — exception().
Это класс исключения, который сообщит вызывающему коду, то есть функции main,
из которой мы и вызываем функцию ParseDate, что пошло что-то не так.
Здесь мы делаем две проверки.
Первую — после того, как считываем год.
Проверяем, что если следующий символ — слэш, то, соответственно,
мы будем его игнорировать.
Если следующий символ — не слэш,
то нам нужно кинуть исключение и сообщить вызывающему коду, то есть функции main,
из которой мы и вызываем эту функцию, что что-то пошло не так.
Замечу, что проверку надо сделать и после того, как мы считаем месяц,
так как там тоже ожидается слэш.
Соответственно, проверяем следующий символ из потока.
Если он не слэш, то бросаем исключение.
Давайте сделаем нашу дату снова правильной, вернем сюда слэши.
Сейчас код должен отработать без ошибок.
Да.
Он отработал.
Сделаем строчку невалидной.
Вот.
Мы видим, что что-то пошло не так,
программа упала, ну, по крайней мере,
код перестал выполняться, и ладно, это уже лучше, чем то, что мы
получаем какую-то странную дату и можем сделать какие-то странные транзакции.
Давайте для начала уберем дублирование,
потому что здесь явно повторяются одни и те же действия.
Мы проверяем следующий символ в потоке, если он не слэш,
то мы просто кидаем исключение, иначе — игнорируем этот символ.
Создадим функцию EnsureNextSymbolAndSkip,
будем сюда передавать строковый поток.
После этого мы
можем прямо вставить туда этот код.
Значит, эта функция просто проверяет,
что следующий символ — слэш И если это слэш, то выкидывает его.
Иначе, бросает исключение.
Соответственно здесь мы должны ее вызвать на нашем потоке,
на нашем строковом потоке.
И вызвать ее еще раз.
Отлично.
Считали год — проверили данные, считали месяц — проверили данные.
Как правильно обрабатывать исключения?
Всем нам понятно, что такое падение программы,
которое сейчас произошло на ваших глазах, это на самом деле плохо.
Представьте, работаете вы в каком-нибудь приложении, сидите в Интернете, вдруг ваш
браузер падает, все вкладки закрылись, закладки потерлись, история потерлась.
Конечно же, это из рук вон плохо.
Все ошибки надо правильно обрабатывать.
Для того чтобы обработать ошибку, в языке C++ есть специальный синтаксис.
Начинается он с ключевого слова try.
Дальше идут фигурные скобки.
В этих фигурных скобках нужно написать тот код,
который потенциально может кидать исключение.
Пока я поставлю просто многоточие.
После блока try идет блок catch.
При этом в catch мы напишем класс,
который мы хотим получить.
То есть catch — мы говорим: поймай следующее исключение.
Все исключения наследуются от класса exception,
поэтому мы можем ловить по exception-ссылкам.
И здесь мы пишем какой-то обработчик нашей ошибки.
Давайте проверим все это на практике.
Опасной функцией у нас является функция ParseDate.
Давайте занесем все это в блок try.
[ЗВУК] После этого сделаем блок catch.
[ЗВУК] И здесь мы напишем,
что что-то пошло не так: exception happens.
Попробуем запустить наш код.
Замечательно, видим строчку: exception happens,
то есть функция проверки данных выбросила исключение,
потому что там не было слэша, а мы его поймали и написали, что произошла беда.
Программа при этом завершается нормально с нулевым кодом возврата.
Давайте заменим обратно на слэши.
Видим, что дата распарсилась и написалась на экран.
Хорошо бы донести до вызывающего кода, в чем произошла ошибка.
То есть хорошо бы в exception как-то записать информацию о том, что
пошло не так — проблема ли в окружении, то есть отсутствует файлик, например.
Мы кидаем исключение, говорим: файл не найден по такому-то пути.
Сразу становится понятно, что там надо просто его создать.
Либо какой-то баг в коде,
что функция ожидает на вход какую-нибудь матрицу фиксированной длины или массив.
Приходит что-то другое, и таким образом она сообщает о том,
что что-то пошло не так.
Для этого есть runtime_error,
внутрь которой можно передать сообщение об ошибке.
И давайте его создадим.
Для этого я использую класс stringstream.
Туда я положу следующий текст: expected
slash, but has.
И запишем собственно символ, который был в потоке.
После этого вызовем метод str,
который из потока вернет строчку, которая в нем записана.
А далее, чтобы получить текст этого сообщения,
мы воспользуемся методом, который есть у класса exception.
Метод называется what.
Если в сообщении есть какой-то текст,
он здесь будет возвращен при вызове метода what.
Запустим наш код, видим, что дата сейчас парсится хорошо.
Давайте попробуем сделать ее плохой.
Видим, что случилось исключение.
Мы ожидали слэш, а получили 97.
На самом деле 97 — это код буквы a.
Давайте приведем к символу то,
что возвращает метод peek, и запустим еще раз.
Данным кодом число, которое здесь возвращается,
преобразуется к char, к символу.
Теперь мы видим, сообщение стало более внятным,
что случилось исключение — мы ожидали слэш, а получили букву a.
Что самое интересное, в том месте, где бросается исключение, было
вызвано уже две функции, начиная с того места, где мы исключение только ловим.
Что я имею в виду?
Смотрите, вот мы объявляем блок try, говорим: ParseDate от данной строчки.
После этого запускается функция ParseDate,
пошла первая степень вложенности, мы уже внутри следующей функции.
Начинаем работу с потоком.
Вызываем функцию EnsureNextSymbolAndSkip, попадаем сюда —
уже вторая степень вложенности от изначальной точки.
И только здесь мы бросаем исключение с нашим сообщением об ошибке.
При этом ловим его уже в функции самого верхнего уровня,
то есть возвращаемся на нулевой уровень и там логируем, что что-то пошло не так.
В этой лекции мы узнали о том,
как работать с исключениями в языке C++, как обрабатывать исключительные ситуации,
которые у вас проявляются, и что для этого нужно сделать.
Теперь мы знакомы с исключениями в языке C++, узнали, когда их нужно применять,
— напомню, это те случаи, когда в вашей программе что-то идет не по плану.
Узнали, как их обрабатывать, познакомились с блоками try и catch.
Напомню, что try — это тот блок, в котором может потенциально произойти какая-нибудь
ошибка, потенциально опасный блок кода, который может выбросить исключение.
А в блоке catch вы его ловите и правильно обрабатываете.
Напомню, что некоторые классы исключений позволяют сохранять в себе ошибку,
в которой содержится описание этой нестандартной ситуации, которая случилась.
В наших примерах это был неправильный разделитель.
И мы сообщали о том, что мы ожидали один разделитель,
а получили на самом деле другой.