[БЕЗ_ЗВУКА] На текущий момент мы с вами посмотрели на умные указатели unique_ptr и shared_ptr и на задачи, которые они умеют решать. Теперь давайте посмотрим на то, как они устроены внутри и как у них получается делать то, что они умеют делать. Начнем с создания unique_ptr на примере вызова функции make_unique. Вот мы вызываем функцию make_unique, она создает новый динамический объект T и получает сырой указатель, который на него ссылается. Затем она создает объект собственно unique_ptr'а и помещает в него этот указатель. Сам указатель ей больше не нужен. Этот unique_ptr она отдает наружу. Вот, в общем-то, и все. Так незамысловато это устроено. unique_ptr действительно очень простой. Указатель на объект — это единственное, что он хранит внутри себя. Посмотрим, как происходит перемещение unique_ptr. У нас создается новый unique_ptr, в который мы перемещаем. Мы берем этот указатель копируем его в новый unique_ptr и зануляем его в исходном unique_ptr. Вот так у нас произошло перемещение. Получилось, что в исходном unique_ptr у нас ничего не осталось, а новый unique_ptr указывает на тот же самый объект. Хорошо, теперь посмотрим, как происходит разрушение unique_ptr'ов. Начнем с unique_ptr, который никуда не указывает. Он собственно берет и умирает, потому что у него указатель никуда не указывает, поэтому никаких дополнительных действий не происходит. Теперь, как у нас умирает unique_ptr, который куда-то указывает? В своем деструкторе он смотрит: так вот указатель, который у меня лежит, он куда-то указывает? В данном случае да, он указывает на динамический объект. Ну отлично. Значит, он вызывает для него delete и удаляет этот динамический объект, после чего удаляется собственно сам unique_ptr. Вот так, с unique_ptr все достаточно просто. Теперь давайте посмотрим, как у нас устроен shared_ptr. Начнем с создания shared_ptr, например, при вызове функции make_shared. Начало точно такое же: у нас создается динамический объект T, оператор new возвращает нам сырой указатель на этот динамический объект, затем создается объект shared_ptr'а, и этот указатель помещается в shared_ptr. А вот после этого происходит интересное. Смотрите, мы помним, что shared_ptr умеет некоторым образом отвечать на вопрос: а существуют ли еще другие shared_ptr'ы, которые указывают на тот же самый объект, то есть у него есть некоторая дополнительная информация. Давайте подумаем, где эта дополнительная информация может находиться. У нас есть сам объект. То есть можно попытаться положить эту информацию в сам объект. Но у нас объект произвольный, по идее его вообще не должно волновать, что он был создан с помощью функции make_shared. Поэтому когда мы говорим о стандартных умных указателях, там в сам объект мы ничего добавить не можем, потому что объект абсолютно произвольный. То есть туда мы добавить эту информацию не можем. У нас есть объект shared_ptr, который создался. Казалось бы, в принципе, вроде бы да, мы можем положить туда произвольную информацию. Однако мы помним, что shared_ptr можно будет скопировать, и скопировать несколько раз. Эти shared_ptr'ы, куда мы скопировали, будут жить, а исходный shared_ptr у нас уже умрет. Получается, что если мы положим какую-то информацию в этот shared_ptr, то тогда она в какой-то момент нам станет уже недоступна. То есть туда ее складывать тоже нельзя. В сам объект складывать нельзя, в shared_ptr складывать нельзя. Что нам остается? Создать некоторое новое место. И действительно, shared_ptr создает в куче новый небольшой утилитарный объект под названием ControlBlock. В этом контрольном блоке он сохраняет счетчик ссылок, то есть текущее количество shared_ptr'ов, которые указывают на этот динамический объект. А у себя, в объекте shared_ptr, он сохраняет только указатель на этот контрольный блок. То есть вот так у нас выглядит раскладка объектов памяти, после того как у нас была вызвана функция make_shared. Временный указатель нам уже не нужен. Получается, что в shared_ptr у нас есть два указателя: и на сам объект, и на контрольный блок. Это очень важно. Давайте теперь посмотрим, как происходит копирование shared_ptr'ов. Создается новый объект shared_ptr. В него копируются указатели на тот же самый объект. Это логично. В него копируются указатели на тот же самый контрольный блок, что тоже логично. Но дальше, поскольку у нас создался новый shared_ptr, который ссылается на тот же самый объект, нам нужно увеличить счетчик ссылок, что и происходит. Мы увеличим счетчик ссылок, теперь он равен 2. Теперь у нас два shared_ptr'а указывают на один и тот же объект, и что характерно, если мы пойдем в любой из этих shared_ptr'ов, мы сможем попасть в один и тот же контрольный блок, где будет записана вот эта информация. Хорошо, давайте посмотрим, как происходит перемещение shared_ptr'ов. Создается новый shared_ptr, в него копируется указатель из того shared_ptr, из которого мы перемещаем. А в исходном shared_ptr этот указатель зануляется, как и в случае с unique_ptr. И то же самое происходит с указателем на контрольный блок. Он копируется и зануляется в исходном shared_ptr. При этом обратите внимание, что счетчик ссылок у нас не меняется, поскольку действительно у нас один shared_ptr стал указывать на этот объект, а другой перестал указывать на этот объект. Поэтому перемещение shared_ptr — это более эффективная операция, чем его копирование. И в принципе, как всегда, перемещение — это в некотором смысле оптимизация операции копирования. Так, давайте теперь посмотрим на разрушение shared_ptr. Начнем с простой ситуации, когда разрушается shared_ptr, который никуда не указывает. Как и в случае с unique_ptr, он просто берет и уничтожается. Это не влечет за собой никаких последствий. Следующим примером рассмотрим удаление shared_ptr, когда существует какой-то другой shared_ptr, указывающий на тот же самый объект. Вот у нас начинает разрушаться shared_ptr, и в своем деструкторе он идет в контрольный блок, и на этот раз он уменьшает счетчик ссылок. Вот теперь их было две, он уменьшает до одной. Смотрит: а есть там еще какие-то ссылки? Да, счетчик не упал до нуля. Если счетчик не упал до нуля, значит, кто-то еще указывает на тот же самый объект, и значит, нам ничего делать больше не нужно. После этого мы просто берем и уничтожаем текущий объект shared_ptr'а. Мы собственно видели, как у нас это происходило, когда мы удаляли указатель e2 на наше дерево. И самый интересный пример — это у нас удаляется последний shared_ptr, указывающий на данный объект. Вот он начинает удаляться, он идет в контрольный блок и уменьшает счетчик ссылок. И видит, что счетчик ссылок на этот раз упал до нуля. Значит, он последний, все, больше никого нет. За нами Москва. Нужно за собой прибраться. Окей. Первым делом он удаляет контрольный блок. В конце концов, контрольный блок он сам же и завел, для того чтобы отследить количество ссылок. После этого он, очевидно, удаляет объект, поскольку именно этим занимаются умные указатели — они удаляют объекты. И вот теперь, когда он за собой прибрался, он может быть удален сам. Таким образом, у нас происходит разрушение shared_ptr. Давайте подведем небольшой итог. unique_ptr — это умный указатель, который нельзя копировать, его можно только перемещать. И unique_ptr всегда удаляет свой объект в деструкторе, если, конечно, объект есть. То есть unique_ptr очень простой, он очень эффективный. Он по сути настолько же эффективный, как и просто сырой указатель, для которого вы в явном виде будете вызывать new delete. shared_ptr хитрее: shared_ptr можно и копировать, и перемещать. И для того чтобы обеспечить возможность копирования, shared_ptr создает контрольный блок, и в этом контрольном блоке он подсчитывает ссылки, которые сейчас ссылаются на этот объект. Когда какой-то shared_ptr разрушается, он уменьшает счетчик ссылок. И в тот момент, когда счетчик ссылок падает до нуля, он удаляет собственно объект. Так работает shared_ptr. Ну и тут нужно сделать небольшой дисклаймер, что это же C++. На самом деле все, конечно же, не так просто. На самом деле unique_ptr работает несколько сложнее. shared_ptr работает намного сложнее, чем здесь было описано. Но это некоторая модель, которой на самом деле вам более чем достаточно для понимания большинства случаев работы с умными указателями и которая вам позволит решать большинство практических задач.