В предыдущем видео нам с вами удалось исправить проблему с помощью использования блока try/catch. Но мы увидели, что у этого подхода есть ряд недостатков. Плохой подход, в общем. Давайте подумаем, как же нам исправить эту проблему без использования блока try/catch. Во-первых, заметим, что проблемы изначально начались из-за того, что мы в явном виде вызываем оператор delete. В самом деле, это ровно то, что мы сделали в блоке catch. Мы вызвали оператор delete и отправили исключение дальше по стеку. Хорошо. Взглянем вообще на нашу систему. У нас есть наш вызывающий код, у нас есть объект, которым мы создаем, и есть указатель, который из нашего кода на этот объект указывает. Мы попробовали вызывать delete на нашей стороне, в коде. Получилось плохо, следовательно, нам не нужно вызывать delete. Что же нам остается? Может быть, нам следует вызвать delete на стороне объекта, который мы создаем? На самом деле такие техники действительно существуют. То есть есть такой подход, когда объект сам себя удаляет, но это очень специфическая и продвинутая техника, и сейчас мы о ней разговаривать не будем. Самое главное, что она нестандартная. Остается что? Остается, на самом деле, указатель, правильно? Хорошо, давайте сделаем так, чтобы указатель, который у нас есть на объект, чтобы он занимался удалением этого объекта. И мы действительно можем это сделать, но не с обычным указателем, обычный указатель так не умеет. Нам понадобится умный указатель. И этим мы сейчас и займемся, мы посмотрим на умный указатель под названием unique_ptr. Давайте продемонстрируем его возможности в небольшой тестовой программе. Для начала мы заведем некоторый класс, который у нас будет уметь говорить нам о том, что с ним происходит. То есть в конструкторе он нам будет выводить, что он создался. Дальше в деструкторе он будет нам говорить, что он умер. Так, еще смайлик добавим, потому что он же рад, что он создался, в конце концов, правильно? И дальше у него будет некоторая функция, чтобы он делал что-нибудь полезное. Так, вот здесь напишем I did some job, сделал что-то хорошее. Окей, вот теперь у нас есть такой класс, и давайте его создадим следующим образом, напишем: auto ptr = new Actor. Мы создаем новый Actor, сохраняем указатель на него в ptr, дальше мы у ptr вызываем DoWork, делаем что-то полезное, и потом мы этот указатель удаляем вот таким образом. Очень простая программка, давайте ее скомпилируем. Здесь мы, собственно, создадим объект, вызовем функцию DoWork и потом его удалим. Так, что... А, забыл точки с запятой. Ну вот, как же так? Хорошо, давайте поставим точки с запятой. Мы же не на Python пишем, в конце концов, нам нужны точки с запятой. Так, хорошо. Давайте запустим нашу программку. Что она выводит? Объект создался, затем что-то сделал и затем умер. Все логично, все нормально. Давайте теперь заведем небольшую функцию, которая... Мы не всегда работаем с объектами напрямую, мы очень часто объект куда-то передаем, и там уже с ним, собственно, как-то работаем. Давайте заведем функцию, которая будет принимать указатель на Actor и будет проверять, если указатель ненулевой, то будет, собственно, выполнять какую-то работу, а если нулевой, то она нам напишет, что мы ожидали Actor, expected. И нас смущает то, что нам передали нулевой указатель. Хорошо, и этот вызов прямой к указателю мы заменим на вызов функции run вот таким образом. Так, хорошо. Программка скомпилировалась, запускаем. Вывод не изменился, мы просто взяли этот вызов, перенесли в функцию. Теперь. Ну вот допустим... Что мы сделали? Мы создали объект, и, допустим, мы забыли вызвать оператора delete. Давайте его просто удалим. Как, я думаю, вы уже прекрасно понимаете, в этом случае объект у нас удаляться не будем, и мы не увидим строчки о том, что объект умирает. Запускаем — да, действительно, объект был создан, что-то сделал и после этого не умер. Давайте теперь вспомним о том, зачем мы здесь собрались. Нам нужен какой-то указатель, который будет уметь сам удалять объект. И вот, собственно, используем unique_ptr. Для этого нам нужен будет заголовочный файл memory. Так, и вот если здесь мы просто создавали Actor, то сейчас мы созданный объект положим в умный указатель следующим образом: unique_ptr<Actor> (new Actor). То есть здесь мы создаем unique_ptr, unique_ptr — это шаблонный класс определенной стандартной библиотеки. Ему мы указываем параметр шаблона — это тип, указатель на который он будет хранить, и дальше, собственно, объект, на который он указывает. В данном случае это новый объект. Эту функцию мы пока закомментируем. Давайте скомпилируем программу. И обратите внимание, что у нас сейчас в этой программе все еще нет явного вызова оператора delete. Но давайте ее запустим. И мы видим, что объект был создан и объект был успешно уничтожен. То есть, действительно, unique_ptr сам позаботился о том, чтобы уничтожить объект, а мы ничего не делали. И это замечательно. Похоже, что мы двигаемся в правильном направлении. Давайте посмотрим, что еще мы можем делать с умным указателем. Попробуем вызвать функцию run, в которую мы передавали обычный указатель, и скомпилируем код. У нас будет ошибка. Он нам говорит, что не может преобразовать unique_ptr<Actor> к Actor*, то есть к обычному указателю. В принципе, логично, это же все-таки разные сущности. Поэтому нам нужно из умного указателя как-то достать обычный указатель на тот объект, на который он указывает. Для этого существует метод get. Давайте теперь напишем здесь метод get, и он вернет нам обычный указатель. Скомпилируем программу, запустим ее. И да, мы видим, что функция успешно вызвана. То есть функция, которая принимает обычный указатель. Действительно, когда мы пишем какой-то код, который использует объект, нам не нужно задумываться о том, этот объект, он каким образом хранится? Или это объект какой-то локальный, или он хранится в обычном указателе, мы вызываем delete, или он управляется умным указателем — нам это не нужно знать, то есть мы просто пишем код и принимаем указатель вот здесь. И, соответственно, если мы используем умный указатель, у нас есть возможность получить обычный указатель на этот объект. Отлично. Давайте посмотрим, что еще мы можем делать с unique_ptr. Давайте для начала попробуем, попробуем его скопировать. Вот у нас будет ptr2, это будет копия ptr. Соберем такую программу. И что она нам скажет? Ага. Она нам скажет, что нет, «не могу я такое скомпилировать, потому что вы пытаетесь использовать конструктор копирования unique_ptr, а у unique_ptr конструктора нет». Он unique не просто так, он уникальный. На один и тот же динамический объект может указывать только один unique_ptr. А здесь мы попытались как бы скопировать указатель, то есть чтобы два unique_ptr указывало на один и тот же объект. Так делать нельзя. Мы не можем копировать unique_ptr, но мы можем перемещать unique_ptr, поэтому здесь мы напишем std::move вот таким образом. Ну ладно, std мы не используем, да? Напишем просто move. То есть теперь мы переместим из ptr в ptr2. Что у нас получится? Так, программа, программа успешно компилируется, все хорошо. Теперь попробуем что-нибудь сделать с этим указателем, который мы получили. Например, запустим на нем ту же самую функцию run. Теперь мы уже делаем ptr2.get. И запустим вот такой код. Так, запускаем. Да, теперь мы видим, что у нас дважды была выполнена работа: первый раз на исходном указателе, второй раз на скопированном указателе. И теперь вопрос к вам: а как вы думаете, что происходит с исходным указателем после того, как мы из него переместили? Как вы, возможно, догадались, когда мы перемещаем из одного unique_ptr в другой unique_ptr, исходный unique_ptr оказывается пустым, потому что других возможностей, на самом деле, практически нет. Указатели должны оставаться уникальными, при этом указатель не будет заниматься копированием объекта. То есть единственное, что мы можем сделать — это обнулить то, на что ссылается исходный умный указатель, исходный unique_ptr, у нас есть другие умные указатели, они могут вести себя немножко по-другому. И давайте, собственно, попробуем вызвать. Здесь мы вызываем функцию run на исходном указателе после того, как мы из него переместили. Запустим такой код. И, действительно, у нас появляется новая строчка, которая говорит о том, что мы вообще-то ожидали некоторый объект, а передали-то нам нулевой указатель. Давайте еще раз взглянем на то, как у нас объявлена функция run. Если указатель оказывается нулевым, она выводит эту строчку. То есть мы убедились, что исходный unique_ptr у нас действительно оказался нулевым после перемещения. Ну и последнее замечание о unique_ptr, которое нам сейчас понадобится, — это то, что создается unique_ptr не таким образом, а для создания unique_ptr используется все-таки специальная функция. Например, смотрите, какая у нас есть проблема в этой записи. Мы написали unique_ptr<Actor>, то есть в явном виде указали тип, и здесь снова написали new Actor же. То есть мы эффективно как бы повторили тот тип, который нам... Тип объекта. Это, на самом деле, если тип большой, это может быть реальной проблемой. И кроме того, с такой записью связаны еще некоторые проблемы, о которых мы поговорим попозже. Но сейчас мы воспользуемся, для создания умного указателя на новый объект специальной функцией make_unique. Она создана именно для этого. То есть вы не должны писать unique_ptr от new какой-то объект, вам следует писать make_unique. Как минимум вы уже понимаете, что это сэкономит вам место на экране. Но кроме того, она обладает еще некоторыми полезными свойствами, о которых мы поговорим попозже. Попробуем собрать такую программу. Программа, на самом деле, получается практически эквивалентной. Запустим ее, и да, у нас все в порядке. Хорошо, давайте подведем небольшой итог. Умный указатель unique_ptr. Его основная функция заключается в том, что в своем деструкторе он сам удаляет тот объект, на который он ссылается. Если он не ссылается ни на какой объект в деструкторе, то есть он был обнулен, тогда он и делать ничего не будет, он просто умрет. И все. То есть unique_ptr, из которого мы переместили, он просто умирает и ничего за собой не удаляет. Мы поняли, что unique_ptr нельзя копировать, его можно только перемещать, потому что он уникальный, вы не можете скопировать его. Мы поняли, что, для того чтобы достать сырой указатель из умного указателя unique_ptr, используется метод get, он возвращает просто T*. И мы узнали, что, для того чтобы создавать unique_ptr на новый объект, нам нужно использовать функцию make_unique. То есть если вы вызываете new T, она возвращает вам обычный указатель на T, или вы вызываете make_unique<T>, она возвращает вам умный указатель unique_ptr на этот T. Вот теперь, пользуясь полученными знаниями, в следующем видео мы займемся тем, что мы сделаем правильные исправления для решения задачи с object pool.