В данном видео мы поговорим об исключениях в языке C++. Мы разберемся зачем нужны исключения, как реализованы исключения в языке C++, как их надо выкидывать и что с ними вообще делать. А также узнаем как обрабатывать исключения в вашем коде. Если кратко, то исключение — это нестандартная ситуация, то есть когда ваш код ожидает, что вокруг него будет какая-то определенная среда или определенные инварианты, они не соблюдаются. Самый банальный пример: у вас есть функция, которая суммирует две матрицы и, например, одна из матриц имеет неправильную размерность. Все. Исключительная ситуация. Здесь можно бросить исключение. Как это сделать? Разберемся прямо сейчас. Давайте рассмотрим следующий пример, когда у нас есть класс даты и мы ее хотим как-то парсить с входного потока. ОК. Что у нас есть в дате? В дате есть год, в дате есть месяц и в дате есть число. Замечательно! Классик мы создали. Что мы хотим? Давайте захотим, например, читать дату с входного потока или давайте, чтобы было совсем просто, будем читать дату из строки. Заведем переменную "date_str" и запишем туда следующую дату: "2017/01/25". Это будет 25 января 2017 года. Отлично! Давайте заведем функцию "ParseDate", которая нам будет возвращать дату, на вход она будет принимать строку. На вход она будет принимать константную строчку, в которой будет храниться текст даты. Значит, что мы здесь дальше будем делать? Мы объявим строковый поток, с которым дальше будем работать. Назовем его "stream" и положим туда входную строчку. После этого заведем переменную даты "date" и начнем из этого потока считывать туда всю необходимую информацию, то есть считаем год. После этого нам надо пропустить следующий символ, как мы помним, там идет слеш. То есть 2017, вот мы его считали в строчке 18, далее идет слеш, потом ноль один, потом слеш, потом 25. Слеш мы можем пропустить с помощью функции "ignore". Сюда надо передать количество символов, которое мы хотим пропустить. После этого аналогичным образом мы считаем месяц и день, то есть еще раз считали год, пропустили слеш, считали месяц, пропустили слеш, считали день. Отлично! Наша дата считана, мы можем вернуть нашу дату. Давайте выведем то, что у нас получится на экран, но для начала объявим перемену типа Date, назовем ее "date" и распарсим строчку выше. После этого с помощью "cout" мы сможем красиво вывести эту дату. В данном случае я устанавливаю ширину поля два и заполнитель ноль, чтобы когда мы вводим числа меньше 10, у нас был красивый заполнитель в виде нуля и дата смотрелась изящно. Выведем день, поставим точку, после этого, на самом деле, мы можем повторить весь тот код и вывести месяц. После этого мы опять можем немного повторить наш код, точнее даже просто вывести год. И не забываем перенос строки. Что ж, запустим наш код. Отлично! Мы видим, что дата распарсилась правильно: "25.01.2017". Вроде задача решена и как-то эта функция у нас работает, но давайте попробуем защититься от ситуации, когда данные на вход могут прийти не в том формате, совсем-совсем не в том, который мы ожидаем. В данном случае мы явно задекларировали, когда ставили задачу, что разделитель — это слеш. Там может быть, например, буква "a", тут может быть буква "b", и это уже не та дата, которую нам надо распарсить, то есть непонятно вообще, дата ли это или это какой-то специальный код. Хорошо бы в таком случае донести до того, кто вызывает эту функцию о том, что формат входных данных — он неправилен. Но сейчас функция этого совершенно не делает, то есть мы запустим данные кода, он скомпилируется, и из этой какой-то странной строчки, которая совсем не похожа на дату, мы извлекли 25 января 2017 года. Почему так получается? Потому что мы считаем сначала 2017 в переменную год, после этого пропустим букву "a", после этого считаем месяц, пропустим букву "b" и считаем день. Как от этой ситуации можно защититься? У потока есть метод "peek", он позволяет посмотреть, какой следующий символ идет в потоке. C одной стороны, мы можем написать много проверок на то, что если функция "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 / , but has: " и запишем собственно символ, который был в потоке. После этого вызовем метод "str", который из потока вернет строчку, которая в нем записана. А далее, чтобы получить текст этого сообщения, мы воспользуемся методом, который есть у класса exception. Метод называется "what". Если в сообщении есть какой-то текст, он здесь будет возвращен при вызове метода "what". Запустим наш код, видим, что дата сейчас парсится хорошо. Давайте попробуем сделать ее плохой. Видим, что случилось: случилось исключение, мы ожидали слеш, а получили 97. На самом деле 97 — это код буквы "a". Давайте приведем к символу то, что возвращает метод "peek" и запустим еще раз. Данным кодом я говорю просто: "Преобразуй число, которое здесь возвращается к char, к символу". Теперь мы видим, сообщение стало более внятным, что случилось исключение, мы ожидали слеш, а получили букву "a". Что самое интересное, в том месте, где бросается исключение было вызвано уже две функции от, начиная с того места, где мы исключение только ловим. Что я имею в виду? Смотрите, вот мы объявляем блок "try", и говорим: "ParseDate от данной строчки". После этого запускается функция: "ParseDate". Пошла первая степень вложенности. Мы уже внутри следующей функции. Начинаем работу с потоком, вызываем функцию: "EnsureNextSymbolAndSkip". Попадаем сюда, уже вторая степень вложенности от изначальной точки и только здесь мы бросаем исключение с нашим сообщением об ошибке. При этом ловим его уже в функции самого верхнего уровня, то есть возвращаемся на нулевой уровень и там логируем, что что-то пошло не так. В этой лекции мы узнали о том, как работать с исключениями в языке C++, как обрабатывать исключительные ситуации, которые у вас появляются и что для этого нужно сделать. Теперь мы знакомы с исключениями в языке C++. Узнали когда их нужно применять. Напомню, это те случаи, когда в вашей программе что-то идет не по плану. Узнали как их обрабатывать, познакомились с блоками "try" и "catch". Напомню, что "try" — это тот блок в котором может потенциально произойти какая-нибудь ошибка, потенциально опасный блок кода, который может выбросить исключение. А в блоке "catch" вы его ловите и правильно обрабатываете. Напомню, что некоторые классы-исключения позволяют сохранять в себе ошибку, в которой содержатся описания этой нестандартной ситуации, которая случилась. В наших примерах это был неправильный разделитель и мы сообщали о том, что мы ожидали один разделитель, а получили на самом деле другой.