Найти в Дзене
ModernCode

C++ Умные указатели (Smart pointers)

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

Минусы обычных указателей (встроенных).

  • Не дают информации о том, указывают ли они на один объект или на массив.
    Например, если вам требуется использовать оператор
    delete, это означает, что нет никакого способа узнать, следует ли использовать его для одного объекта(delete) или для массива (delete[ ]). При использовании оператора неверного вида результат будет неопределенным.
  • Не говорят о том, должны ли вы уничтожить то, на что они указывают, когда программа завершает работу, т.е. владеет ли указатель тем, на что указывает.
  • Нет возможности узнать, каким именно способом вы должны уничтожить то, на что указывает указатель.
  • Очень трудно обеспечить удаление объекта ровно один раз на каждом пути вашего кода. Пропущенный путь ведет к утечке ресурсов, а выполнение удаления более одного раза - к неопределенному поведению.
  • Нет способа выяснить, не является ли указатель висячим.

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

В С++14 существует 4 умных указателя: std::auto_ptr, std::unique_ptr, std::shared_ptr, std::weak_ptr. Все они призваны помочь в управлении временем жизни динамически создаваемых объектов, то есть избежать утечек, при этом гарантируя, что объекты будут удалены определенным образом в определенный момент, включая генерацию исключений.

std::auto_ptr является устаревшим указателем, пришедшим из С++98. Попытка его стандартизации привела к тому, что в С++11 он превратился в std::unique_ptr. Правильное выполнение некоторых операций требовало семантики перемещений, которой на тот момент времени в С++98 не было. В качестве быстрого и необходимого решения разработали std::auto_ptr, который превращал операцию копирования в перемещение. Но тогда существовали серьезные ограничения: его нельзя было хранить в контейнерах и копирование превращало его в нулевой указатель.

std::unique_ptr делает все то же, что и std::auto_ptr, но намного эффективнее и во всех отношениях лучше. Единственный случай использования std::auto_ptr - необходимость компиляции кода на С++98.

Рассмотрим каждый из умных указателей.

std::unique_ptr:

  • получает единоличное владение объектом через указатель и разрушает его, когда он выходит из области видимости;
  • не может быть скопирован или задан через операцию присваивания;
  • неконстантный std::unique_ptr может передавать управление объектом другому подобному указателю;
  • константный std::unique_ptr не может быть передан областью, в которой был создан, ограничивая время жизни управляемого объекта областью видимости;
  • когда std::unique_ptr уничтожается, он удаляет объект с помощью custom deleter.

Существует 2 варианта управления временем жизни объекта:

  1. для одного объекта, к примеру, созданного оператором new;
  2. для массива с длиной, определенной во время выполнения, созданного с помощью new[ ];

std::unique_ptr используют :

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

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

Рассмотрим пример:

std::shared_ptr:

  • умный указатель с разделяемым владением объекта;
  • несколько std::shared_ptr могут владеть одним и тем же объектом;
  • объект будет высвобожден, когда последний std::shared_ptr, указывающий на него, удалится или сбросится;
  • объект уничтожается с использованием delete_expression или пользовательской функции удаления объекта, переданной в конструктор.

std::shared_ptr может не владеть ни одним объектом, в этом случае он называется пустым. Он также отвечает требованиям CopyConstructible и CopyAssignable.

В стандартной реализации std::shared_ptr содержит только 2 указателя:

  • на объект владения;
  • на блок управления, т.е. на динамически созданный объект, который включает сам управляемый объект или указатель на него, функцию удаления, аллокатор , счетчик указателей std::shared_ptr и std::weak_ptr;

Когда std::shared_ptr создается:

  • вызовом std::make_shared или std::allocated_shared, блок управления содержит сам управляемый объект в качестве члена структуры данных;
  • вызовом его конструктора, блок управления содержит указатель на управляемый объект.

Размер std::shared_ptr в два раза больше, чем у обычного указателя. Это связано с тем, что он хранит в себе обычный указатель и указатель на счетчик ссылок.

Рассмотрим пример:

-2

std::weak_ptr:

- содержит “слабую” ссылку на объект, управляемый указателем std::shared_ptr;

  • моделирует временное владение, когда объект должен быть доступен, только если он существует
  • используется для отслеживания объекта;
  • преобразуется в std::shared_ptr для принятия временного владения;
  • используется для устранения циклических ссылок std::shared_ptr.

Рассмотрим пример:

-3

Встроенные указатели являются острым инструментом: достаточно малейшей невнимательности и можно очень сильно порезаться.

Советы по использованию умных указателей:

  1. Используйте std::unique_ptr для единоличного владения, а std::shared_ptr - совместного владения.
  2. Используйте std::weak_ptr для указателей std::shared_ptr, которые могут быть висячими.
  3. Предпочтительнее использовать std::make_shared и std::make_unique, а не new.
  4. Избегайте создания указателей std::shared_ptr из переменных, тип которых - обычный встроенный указатель.