Взгляд на справочную семантику

В этой статье я постараюсь представить концепцию и мотивацию использования указателей. Есть группы разработчиков C++, которые используют интеллектуальные указатели только из соображений безопасности, другие используют необработанные указатели только для повышения производительности, а некоторые используют и то, и другое, когда они обеспечивают максимальную выгоду. В этой статье я стремлюсь осветить варианты использования указателей, чтобы вы могли решить, когда их использовать.

Концептуально указатель — это своего рода ссылка. То есть указатель относится к определенному блоку памяти, который выступает точкой входа. Чтобы получить доступ к содержимому или изменить значение указателя, мы разыменовываем его. Многие языки имеют функции, подобные указателям. Наша забота вращается вокруг контекста C и C++. Обратите внимание, что все функции C допустимы в C++, но обратное неверно. По этой причине наш раздел об интеллектуальных указателях применим только к C++.

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

Указатели введены в C по следующим причинам:

  • Недорогая передача параметров.
  • Выделите память для новых объектов в куче.
  • Передача ссылки на функцию в функцию.
  • Итерация по структурам данных.

То же самое верно и в C++. По сути, итераторы (компонент для перебора контейнеров C++) напоминают интерфейс указателя. Но C++ может предложить больше с другим набором модальностей, с помощью которых можно выразить семантику ссылок. Мы сосредоточимся на наборе эталонных модальностей, известных как интеллектуальные указатели. Но перед этим давайте представим убедительную причину использования интеллектуальных указателей, перечислив проблемы необработанных указателей.

Необработанные указатели

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

Некоторые языки в той или иной степени имеют ограничения на типы указателей. Например, Ада инициализирует значение по умолчанию для своих типов указателей равным нулю и накладывает ограничение на преобразования типов с использованием указателей в основном для безопасности типов. В то время как такие языки, как Java, запретили использование низкоуровневых типов указателей для уменьшения ошибок программирования.

В нем ползают тонкие оттенки указателей. Теперь мы спросим, ​​какова сцена типов указателей в C?

В таких языках, как C и C++, типы указателей очень часто используются для низкоуровневого программирования. Преобразование типов указателей имеет разную степень ограничений. В C типы void * рассматриваются как необработанные типы указателей, которые могут быть преобразованы в любой тип с помощью операции, называемой приведением типов.

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

Необработанные указатели являются конкретной реализацией более абстрактной концепции ссылки. В отличие от Ada, необработанные указатели в C/C++ не инициализируются нулевым значением, хотя они могут ссылаться на null, что является более безопасным способом использования необработанных указателей: если они не инициализированы адресом референта, инициализируйте его с помощью nullptr (в C++ ) и NULL (в C). Причина этого заключается в том, чтобы добиться четко определенного поведения и избежать ошибок, которые может быть очень трудно исправить, не говоря уже о том, чтобы найти.

Необработанные указатели ничего не навязывают, поэтому их неправильное использование или злоупотребление ими очень легко совершить.

Висячая проблема:

Давайте рассмотрим случай, когда мы, вероятно, столкнемся с проблемой висячего указателя. Давайте посмотрим на выделение и освобождение ресурсов с помощью функций new и delete в C++ и malloc (calloc) или free в C.

Обратите внимание, что функции delete или free только удаляют содержимое объекта, в данном случае i. То есть разыменование i по-прежнему возвращает место в памяти i до его удаления. Это проблематично, поскольку может привести к неопределенному поведению: разыменование уже освобожденной памяти опасно. Чтобы решить эту проблему, мы должны убедиться, что i ссылается на четко определенное место в памяти, поэтому мы помещаем i = nullptr (в C++) и i = NULL (в C) после удаления содержимого i, чтобы избежать критических ошибок.

Другая проблема связана с определением указателя-владельца, что может привести к серьезным проблемам, от утечек ресурсов, неопределенного поведения до уязвимостей в системе безопасности. Какой указатель за какую сущность отвечает? Трудно отслеживать несколько ссылок и убедиться, что в конце области действия такие ссылки правильно обрабатываются. Должно быть четкое выражение для определения указателя-владельца. По этой причине C++ представил систему управления ссылками, основанную на идее RAII.

Умные указатели

Из Стандарта С++:

Применение профиля безопасности на протяжении всего срока службы устраняет утечки. В сочетании с безопасностью ресурсов, обеспечиваемой RAII, это устраняет необходимость в «сборке мусора» (за счет отсутствия генерации мусора). Объедините это с соблюдением профилей типов и границ, и вы получите полную безопасность типов и ресурсов, гарантированную инструментами.

C++ представил набор классов для автоматического управления ресурсами с минимальной или нулевой потерей производительности. Использование этого набора указателей имеет следующий набор преимуществ:

  • ясность выражения собственности
  • гарантирует, что ресурсы не будут утекать

Когда это уместно, мы должны использовать интеллектуальные указатели для четкого выражения наших намерений с тем видом собственности, который мы хотим установить между указателем и сущностью, на которую он ссылается. У нас есть три умных указателя: std::unique_ptr<T>, std::shared_ptr<T> и std::weak_ptr<T>. Мы можем получить доступ к этим модальностям в заголовочном файле <memory>.

Давайте пройдемся и узнаем, что могут предложить эти эталонные модальности. Для нашей демонстрации давайте рассмотрим класс с именем Entity, определенный как:

Уникальный указатель

Уникальный указатель утверждает, что за данный ресурс в памяти отвечает только один указатель. Указатель-владелец отвечает за очистку и управление ресурсами.

Давайте определим экземпляр Entity.

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

Передача уникального указателя с конструкторами перемещения — это передача владения уникальному указателю, определенному в функции f(). Мы можем заметить поведение, запустив следующий фрагмент:

Поскольку мы передали право собственности на e entity, очистка произошла, как только сущность вышла из функции.

Общий указатель

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

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

Проблема круговой ссылки

Циклическая ссылка имеет форму A→B, B→A. Давайте поместим это в код.

Поскольку A→B и B→A, счетчик ссылок никогда не будет равен нулю — условие, при котором std::shared_ptr<T> выполняет свою очистку. Мы можем наблюдать за подсчетом ссылок с помощью функцииuse_count(). Изменим основную функцию:

Как нам разрушить проклятие? Знакомство со слабыми указателями.

Слабый указатель

Слабый указатель — это особый тип указателя в сочетании с общими указателями. С помощью слабых указателей вы можете получить доступ к содержимому общего указателя без увеличения счетчика ссылок. Это полезно для наблюдения за содержимым общего ресурса и, в первую очередь, для решения проблемы циклической ссылки.

Решим проблему циклической ссылки через слабые указатели. Этого можно добиться, изменив указатели A::ab или B::ba.

Резюме

C++ не использует автоматическое управление ссылками по умолчанию, следуя своей философии дизайна принцип нулевых накладных расходов. В C++ можно включить автоматическое управление ссылками, используя интеллектуальные указатели, определенные в стандартной библиотеке. Это дает вам ресурсные гарантии и безопасность. Другие языки, такие как Swift и Rust, придерживаются той же концепции вместо создания сложных сборщиков мусора, они вообще не производят никакого мусора. Эта модель управления памятью является убедительным примером использования интеллектуальных указателей. Но у необработанных указателей все еще есть свои самые яркие моменты.

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

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

Стандарт рекомендует следующее:

  • Посмотрите на указатели: классифицируйте их на невладельцев (по умолчанию) и владельцев. Там, где это возможно, замените владельцев дескрипторами ресурсов стандартной библиотеки (как в приведенном выше примере). Либо отметьте владельца как такового, используя владельца из GSL.
  • Ищите голых new и delete
  • Ищите известные функции распределения ресурсов, возвращающие необработанные указатели (например, fopen, malloc и strdup).

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

  1. Барнс, Дж. (2005). Безопасные указатели. АдаКор. Получено с: adacore.com/uploads_gems/03_safe_secure_ada
  2. Компьютерофил (2017). Почему Си так влиятельна? Получено с: youtube.com/watch?v=ci1PJexnfNE.
  3. Документы Microsoft (2020). Необработанные указатели (C++).
  4. Документы Microsoft (2020). Умные указатели (современный C++).
  5. Документы Майкрософт (2019). Срок службы объекта и управление ресурсами (RAII).
  6. Страуструп, Б. и Саттер, Х. (2021). Основные принципы С++. Получено из: isocpp.github.io/CppCoreGuidelines/CppCoreG..???
  7. Страуструп, Б. (2013). Язык программирования С++. Пирсон Образование.

Первоначально опубликовано по адресу: https://dcode.hashnode.dev