Реализация строительных блоков семантики ссылок: уникальная ссылка

Наше предыдущее обсуждение исследует анатомию типов указателей в C и C++ наравне с намеками на нюансы семантики ссылок. Мы обсудили случаи использования необработанных указателей и интеллектуальных указателей в современном C++ и обнаружили, что существует определенная степень пригодности при выборе между автоматическим управлением ресурсами и ручным управлением ресурсами. В частности, мы представили std::unique_ptr<T> и продемонстрировали некоторые его функции.

В этой статье обсуждаются цели разработки и реализация std::unique_ptr<T>. Однако вместо того, чтобы обсуждать тонкости реализации STL в различных дистрибутивах компилятора C++, мы упростим это, чтобы выразить цели разработки уникального справочника.

В конце этой статьи мы разработаем еще один уровень понимания с функциями уникальной ссылки и реализуем нашу версию.

Цели дизайна уникальной ссылки

Перечислим требования, которым мы должны удовлетворять для реализации нашей версии уникальной ссылки. Наша реализация должна удовлетворять следующему.

  • Должен принимать любой тип
  • Автоматическое управление ресурсами
  • Сохранить свойство уникальности
  • Интерфейс, похожий на указатель

Наша простая версия std::unique_ptr<T> требует явной инициализации объекта, на который она ссылается.

Выполнение

Давайте создадим нашу реализацию, выполняя наши требования к дизайну одно за другим.

Требование: Должен принимать любой тип

Начнем с самого простого требования *Он должен принимать любой тип*. Нам просто нужно добавить параметр шаблона к нашему типу (классу), чтобы этого требования было достаточно.

Вот и все! Мы выполнили наше первое требование.

Требование: автоматическое управление ресурсами

Это элегантное решение механизма абстракции C++, которое появляется: ответ Приобретение ресурсов — это инициализация или RAII.

RAII предоставляет гарантии относительно состояния нашего класса. Управление ресурсами связано с неизменностью класса в течение всего времени жизни объекта. Это достигается с помощью конструкторов и деструкторов объектов, которые отвечают за инициализацию и очистку ресурсов.

Конструктор запускается при инициализации экземпляра unique_reference — вот почему в других языках, таких как Python и Swift, они известны как инициализаторы. Между тем, деструкторы запускаются, когда экземпляр unique_reference выходит за пределы области видимости. Вместе они удовлетворяют нашу потребность в автоматическом управлении ресурсами, которое дает нам детерминированное поведение, гарантирующее управление ресурсами. Детерминированное поведение нашего управления ссылками дает превосходное свойство по сравнению с реализацией сборщиков мусора, которые могут очищать или не очищать часть ресурса в тот момент, когда объект выходит из области видимости.

Кажется, мы хорошо распорядились своими ресурсами. Мы инициализировали наш указатель на nullptr при построении и деинициализировали его, удалив содержимое m_ptr. Тем не менее, есть небольшая проблема, связанная с тем, что мы все еще можем утечь наши ресурсы, даже после установки m_ptr = nullptr. Компилятор вас этим не предупредит, но вы стреляете себе в ногу.

Чтобы решить эту проблему, нам нужно переосмыслить нашу ментальную модель уникальной ссылки. Когда наш объект выходит из области видимости, мы хотим уничтожить объект, а не обязательно удалить его внезапно. Поэтому мы вызываем деструктор объекта ~T().

Обратите внимание, что типы, не имеющие деструкторов, предоставленных пользователем, такие как примитивы, помечаются как тривиальные деструкторы, поэтому ~T() допустимо для примитивов.

У нас все отлично. Давайте продолжим!

Требование: поддерживать свойство уникальности

Для этого мы хотим, чтобы наша уникальная ссылка могла передавать свое право собственности внутри функции или класса, когда это необходимо. Реализуя механизм переноса, нам необходимо сохранить свойство уникальности. Мы делаем это, включив семантику перемещения. Есть две части для включения перемещаемого владения, наша уникальная ссылка должна быть: перемещать конструируемую и перемещать назначаемую.

Давайте сначала поработаем с нашими подвижными конструкторами.

Давайте пройдемся по тому, что мы на самом деле говорим, с помощью приведенного выше фрагмента кода. Во-первых, нам нужно пометить наши конструкторы как explicit, чтобы избежать неожиданных неявных преобразований. Затем мы используем ссылку на значение rvalue для нашего параметра, который помечает наш класс как конструируемый для перемещения. Но прежде чем мы займемся построением перемещения, мы должны гарантировать, что при построении мы не будем генерировать исключения, поэтому мы помечаем наш спецификатор noexcept.

Тело нашей функции очень просто реализовать. Мы хотим взять все ресурсы нашей перемещенной ссылки (other) и поменять их местами с нашим перемещенным объектом (this). Тем не менее, второе требование (назначаемое перемещение) столь же тривиально реализовать. Нам просто нужно перегрузить оператор присваивания.

По сути, мы повторяем тело нашего присваивания и конструктора, поэтому для согласованности давайте реализуем небольшую вспомогательную функцию, которую мы объявляем в приватном режиме, которая будет отвечать за замену объектов.

Фактически это приводит к:

Поскольку мы обсуждаем конструкторы и операторы присваивания, стоит отметить, как их подавить и почему. Причина, по которой мы хотим подавить конструкторы, состоит из двух частей: мы хотим явно указать, что не собираемся этого делать, и мы хотим, чтобы компилятор проверял, пытаются ли пользователи получить доступ к таким модальностям.

Для этого просто пишем:

Здесь мы подавили присваивание и построение копирования, поскольку копирование не согласуется с понятием уникальности.

Требование: указательный интерфейс

Нам нужен интерфейс для связи с состоянием нашей уникальной ссылки. Для согласованности он должен напоминать интерфейс указателя.

Напомним, что указатель можно разыменовать с помощью операторов * и ->. И нам нужен оператор & для проверки расположения нашего указателя в памяти. Это основные операторы, которые нам нужно перегрузить для нашей уникальной ссылки. Для этого пишем:

Пройдемся по трем линиям.

Первая строка возвращает ссылку *(this->m_ptr), что означает доступ к содержимому m_ptr, которое мы можем изменять и читать. Та же идея и с оператором стрелки, мы возвращаем указатель на расположение m_ptr в памяти. Последний оператор немного отличается тем, что возвращает адрес указателя, а не референта. Напомнить, что указатель имеет свое место в памяти отдельно от объектов, на которые он указывает.

Собираем все вместе:

Тестовые случаи

Пришло время проверить, удовлетворили ли мы наши требования к дизайну:



Я еще не уверен, работает ли встраивание repl.it на вашей стороне. Если это недоступно с вашей стороны, дайте мне знать.

Резюме

Мы конкретизировали наши требования к дизайну и реализовали нашу версию уникальной ссылки, чтобы удовлетворить то, что мы собираемся делать с уникальными ссылками. После тестирования мы обнаружили, что он делает то, что нам нужно: теперь у нас есть первая часть нашего автоматического управления ресурсами!

Так как мы реализовали простой уникальный ссылочный класс, попробуйте подумать о том, как мы можем расширить его для интерфейса, подобного массиву, в качестве домашнего задания. Самостоятельное выполнение этой части дает вам возможность переосмыслить то, что мы сделали с нашим unique_reference.

Получайте удовольствие от взлома!

использованная литература

- Авторы Википедии. (2021, 21 мая). Получение ресурсов — это инициализация. В Википедии, свободной энциклопедии. Получено 07:06, 17 августа 2021 г., с https://en.wikipedia.org/w/index.php?title=Resource_acquisition_is_initialization&oldid=1024395395.

- Питон (2021). Справочное руководство по настройке инициализации Python. https://docs.python.org/3/c-api/init_config.html.

- Стриж (2021). Руководство по языку Swift. https://docs.swift.org/swift-book/LanguageGuide/Initialization.html.

- cppreference.com (2021). Деструкторы. https://en.cppreference.com/w/cpp/language/destructor.

- cppreference.com (2021). Фундаментальные типы. https://en.cppreference.com/w/cpp/language/types.

- StackOverflow (2010). Что такое семантика перемещения? https://stackoverflow.com/questions/3106110/what-is-move-semantics.

- Переполнение стека (2009 г.). Что означает явное ключевое слово? https://stackoverflow.com/questions/121162/что означает явное ключевое слово.

- Треугольники (2019). C++ ссылки на rvalue и семантика перемещения для начинающих. https://www.internalpointers.com/post/c-rvalue-references-and-move-semantics-beginners.

- Амиана, Д. (2021). Указатели и ссылки: цели разработки и варианты использования. https://dcode.hashnode.dev/pointers-and-references-design-goals-and-use-cases.