Smart pointers для начинающих
Эта небольшая статья в первую очередь предназначена для начинающих C++ программистов, которые либо слышали об умных указателях, но боялись их применять, либо они устали следить за new-delete.
UPD: Статья писалась, когда C++11 еще не был так популярен.
- утечки памяти;
- разыменовывание нулевого указателя, либо обращение к неициализированной области памяти;
- удаление уже удаленного объекта;
Введение
Существует техника управления ресурсами посредством локальных объектов, называемая RAII. То есть, при получении какого-либо ресурса, его инициализируют в конструкторе, а, поработав с ним в функции, корректно освобождают в деструкторе. Ресурсом может быть что угодно, к примеру файл, сетевое соединение, а в нашем случае блок памяти. Вот простейший пример:
Это удобно: по выходу из функции нам не нужно заботиться об освобождении буфера, так как для объекта screen вызовется деструктор, который в свою очередь освободит инкапсулированный в себе массив пикселей. Конечно, можно написать и так:
В принципе, никакой разницы, но представим себе такой код:
Придется в каждой ветке выхода из функции писать delete [], либо вызывать какие-либо дополнительные функции деинициализации. А если выделений памяти много, либо они происходят в разных частях функции? Уследить за всем этим будет все сложнее и сложнее. Подобная ситуация возникает, если мы в середине функции бросаем исключение: гарантируется, что объекты на стеке будут уничтожены, но с кучей проблема остается открытой.
Ок, будем использовать RAII, в конструкторах инициализировать память, в деструкторе освобождать. И пусть поля нашего класса будут указателями на участки динамической памяти:
Теперь представьте, что полей не 3, а 30, а значит в деструкторе придется для всех них вызывать delete. А если мы второпях добавим новое поле, но забудем его убить в деструкторе, то последствия будут негативными. В итоге получается класс, нагруженный операциями выделения\освобождения памяти, да еще и непонятно, все ли было правильно удалено.
Поэтому предлагается использовать умные указатели: это объекты, которые хранят указатели на динамически аллоцированные участки памяти произвольного типа. Причем они автоматически очищают память по выходу из области видимости.
Сначала рассмотрим то, как они выглядят в С++, затем перейдем к обзору некоторых распространенных типов умных указателей.
Простейший smart pointer
- boost::scoped_ptr
- std::auto_ptr
- std::tr1::shared_ptr (он же std::shared_ptr в C++11, либо boost::shared_ptr из boost)
boost::scoped_ptr
Он находится в библиотеке буст.
Реализация простая и понятная, практически идентичная нашей, за несколькими исключениями, одно из них: этот пойнтер не может быть скопирован (то есть у него приватный конструктор копирования и оператор присваивания). Поясню на примере:
Оно и понятно, если бы было разрешено присваивание, то и p1 и p2 будут указывать на одну и ту же область памяти. А по выходу из функции оба удалятся. Что будет? Никто не знает. Соответственно, этот пойнтер нельзя передавать и в функции.
Тогда зачем он нужен? Советую применять его как указатель-обертка для каких-либо данных, которые выделяются динамически в начале функции и удаляются в конце, чтобы избавить себя от головной боли по поводу корректной очистки ресурсов.
Подробное описание здесь.
std::auto_ptr
Чуть-чуть улучшенный вариант предыдущего, к тому же он есть в стандартной библиотеке (хотя в C++11 вроде как deprecated). У него есть оператор присваивания и конструктор-копировщик, но работают они несколько необычно.
Поясняю:
Теперь при присваивании в p2 будет лежать указатель на MyObject (который мы создавали для p1), а в p1 не будет ничего. То есть p1 теперь обнулен. Это так называемая семантика перемещения. Кстати, оператор копирования поступает таким же образом.
Зачем это нужно? Ну например у вас есть функция, которая должна создавать какой-то объект:
Это означает, что функция создает новый объект типа MyObject и отдает его вам в распоряжение. Понятней станет, если эта функция сама является членом класса (допустим Factory): вы уверены, что этот класс (Factory) не хранит в себе еще один указатель на новый объект. Объект ваш и указатель на него один.
В силу такой необычной семантики auto_ptr нельзя использовать в контейнерах STL. Но у нас есть shared_ptr.
std::shared_ptr (С++11)
Умный указатель с подсчетом ссылок. Что это значит. Это значит, что где-то есть некая переменная, которая хранит количество указателей, которые ссылаются на объект. Если эта переменная становится равной нулю, то объект уничтожается. Счетчик инкрементируется при каждом вызове либо оператора копирования либо оператора присваивания. Так же у shared_ptr есть оператор приведения к bool, что в итоге дает нам привычный синтаксис указателей, не заботясь об освобождении памяти.
Теперь и p2 и p1 указывают на один объект, а счетчик ссылок равен 2, По выходу из скоупа счетчик обнуляется, и объект уничтожается. Мы можем передавать этот указатель в функцию:
Заметьте, если вы передаете указатель по ссылке, то счетчик не будет увеличен. Вы должны быть уверены, что объект MyObject будет жив, пока будет выполняться функция test.
Итак, smart pointers это хорошо, но есть и минусы.
Во-первых это небольшой оверхед, но я думаю у вас найдется несколько тактов процессора ради такого удобства.
Во-вторых это boiler-plate, например
Это частично можно решить при помощи дефайнов, допустим:
Либо при помощи typedef.
В-третьих, существует проблема циклических ссылок. Рассматривать ее здесь не буду, чтобы не увеличивать статью. Так же остались нерассмотренными boost::weak_ptr, boost::intrusive_ptr и указатели для массивов.
Кстати, smart pointers достаточно хорошо описаны у Джеффа Элджера в книге «С++ for real programmers».
Записки программиста
Благодаря наличию исключений, язык C++ позволяет разделить основную логику приложения и обработку ошибок, не мешая их в одну кучу. Что есть очень хорошо. Однако теперь по коду нельзя с уверенностью сказать, где может быть прервано его исполнение. Отсюда возникает опасность утечки ресурсов. Проблема эта решается при помощи деструкторов и идиомы RAII. Впрочем, придерживаться этой идиомы становится проблематично при использовании указателей. Особенно при использовании их не как членов класса, а просто как переменных в методах. На наше с вами счастье, в стандартной библиотеке языка есть умные указатели (smart pointers), придуманные именно для этого случая. Поскольку на C++ я пишу не регулярно, то иногда забываю некоторые нюансы использования умных указателей, в связи с чем решил вот набросать небольшую шпаргалку.
Важно! В старых книжках и статьях можно встретить упоминание auto_ptr. Этот тип умных указателей появился в C++, когда в языке еще не было move semantics. Из-за этого использование auto_ptr порой может приводить к трудным в обнаружении ошибкам. В стандарте C++17 auto_ptr был удален. Другими словами, все, что вы должны знать об auto_ptr — это то, что его не должно быть в современном коде. Вместо него всегда используйте unique_ptr.
unique_ptr
Шаблонный класс unique_ptr представляет собой уникальный указатель на объект. Указатель нельзя копировать, но можно передавать владение им с помощью std::move. При уничтожении указателя автоматически вызывается деструктор объекта, на который он указывает.
Создается unique_ptr так:
… но обычно используют шаблон make_unique, так короче:
Класс unique_ptr перегружает оператор -> , что позволяет обращаться к полям класса и вызывать его методы, словно мы работаем с обычным указателем:
Как уже отмечалось, unique_ptr запрещено копировать:
Однако владение им можно передать при помощи std::move, например:
Плюс к этому, мы всегда можем получать из unique_ptr обычный указатель на объект:
… хотя это и является code smell. Кроме того, ничто не мешает создавать ссылки (reference) на unique_ptr:
То есть, в этом случае мы как бы не отнимаем владение объектом, а ненадолго одалживаем его, обращаясь к нему через все тот же умный указатель.
Интересно, что unique_ptr позволяет указать функцию, которую он будет вызывать вместо деструктора, так называемый custom deleter. Это позволяет использовать unique_ptr с ресурсами, возвращаемых из библиотек для языка C, и даже реализовать аналог defer из языка Go:
/* g++ custom-deleter.cpp -o custom-deleter */
#include <memory>
#include <functional>
#include <iostream>
#include <stdio.h>
template < typename T >
using auto_cleanup = std :: unique_ptr < T,std :: function < void ( T * ) >> ;
#define _DEFER_CAT_(a,b) a##b
#define _DEFER_NAME_(a,b) _DEFER_CAT_(a,b)
#define defer(. ) \
auto _DEFER_NAME_(_defer_,__LINE__) = \
auto_cleanup<char>(dummy, [&](char*) < __VA_ARGS__; >);
Заметьте, что в макросе defer нам пришлось передать в unique_ptr фиктивный указатель. Если бы мы передали nullptr, custom deleter не был бы вызван.
Важно! Если в умном указателе вы держите указать на массив объектов, то обязаны указать custom deleter, вызывающий для этого массива delete[] вместо delete . Если этого не сделать, будет освобожден только первый объект из массива, остальные же утекут.
shared_ptr и weak_ptr
Класс shared_ptr является указатем на объект, которым владеет сразу несколько объектов. Указатель можно как перемещать, так и копировать. Число существующих указателей отслеживается при помощи счетчика ссылок. Когда счетчик ссылок обнуляется, вызывается деструктор объекта. Сам по себе shared_ptr является thread-safe, но он не делает магическим образом thread-safe объект, на который ссылается. То есть, если доступ к объекту может осуществляться из нескольких потоков, вы должны не забыть предусмотреть в нем мьютексы или что-то такое.
Для создания shared_ptr обычно используется шаблон make_shared:
В остальном работа с ним мало отличается от работы с unique_ptr, за тем исключением, что shared_ptr можно смело копировать.
Интересные грабли при использовании shared_ptr заключается в том, что с его помощью можно создать циклические ссылки. Например, есть два объекта. Первый ссылается при помощи shared_ptr на второй, а второй — на первый. Даже если ни на один из объектов нет других ссылок, счетчики ссылок никогда не обнулятся, и объекты никогда не будут уничтожены.
Эта проблема обходится при помощи weak_ptr, так называемого слабого указателя. Класс weak_ptr похож на shared_ptr, но не участвует в подсчете ссылок. Также у weak_ptr есть метод lock() , возвращающий временный shared_ptr на объект. Пример использования:
#include <memory>
#include <iostream>
int main ( ) {
std :: weak_ptr < SomeClass > wptr ;
{
auto ptr = std :: make_shared < SomeClass > ( ) ;
wptr = ptr ;
SomeClass
lock() failed
Можно думать о weak_ptr как об указателе, позволяющим получить временное владение объектом. Само собой разумеется, если все постоянные указатели на объект перестанут существовать, и останутся только временные, полученные при помощи метода lock() класса weak_ptr, объект продолжит свое существование. Он будет уничтожен только тогда, когда на объект не останется вообще никаких указателей.
Умные указатели и наследование
Вопрос, о котором часто забывают — это кастование умных указателей вверх и вниз по иерархии классов. Для shared_ptr в стандартной библиотеке есть шаблоны static_pointer_cast, dynamic_pointer_cast и другие. Для unique_ptr таких же шаблонов почему-то не занесли, но их нетрудно найти на StackOverflow.
#include <memory>
#include <iostream>
// https://stackoverflow.com/a/21174979/1565238
template < typename Derived, typename Base, typename Del >
std :: unique_ptr < Derived, Del >
static_unique_ptr_cast ( std :: unique_ptr < Base, Del > && p )
{
auto d = static_cast < Derived * > ( p. release ( ) ) ;
return std :: unique_ptr < Derived, Del > ( d,
std :: move ( p. get_deleter ( ) ) ) ;
}
template < typename Derived, typename Base, typename Del >
std :: unique_ptr < Derived, Del >
dynamic_unique_ptr_cast ( std :: unique_ptr < Base, Del > && p )
{
if ( Derived * result = dynamic_cast < Derived * > ( p. get ( ) ) ) {
p. release ( ) ;
return std :: unique_ptr < Derived, Del > ( result,
std :: move ( p. get_deleter ( ) ) ) ;
}
return std :: unique_ptr < Derived, Del > ( nullptr, p. get_deleter ( ) ) ;
}
class Derived : public Base {
public :
Derived ( int num ) : Base ( num ) { }
void testUnique ( ) {
std :: cout << "=== testUnique begin ===" << std :: endl ;
auto derived = std :: make_unique < Derived > ( 1 ) ;
derived — > sayHello ( ) ;
std :: unique_ptr < Base > base = std :: move ( derived ) ;
base — > sayHello ( ) ;
auto derived2 = static_unique_ptr_cast < Derived > ( std :: move ( base ) ) ;
derived2 — > sayHello ( ) ;
std :: unique_ptr < Base > base2 = std :: make_unique < Derived > ( 2 ) ;
base2 — > sayHello ( ) ;
void testShared ( ) {
std :: cout << "=== testShared begin ===" << std :: endl ;
auto derived = std :: make_shared < Derived > ( 1 ) ;
derived — > sayHello ( ) ;
auto base = std :: static_pointer_cast < Base > ( derived ) ;
base — > sayHello ( ) ;
auto derived2 = std :: static_pointer_cast < Derived > ( base ) ;
derived2 — > sayHello ( ) ;
std :: shared_ptr < Base > base2 = std :: make_shared < Derived > ( 2 ) ;
base2 — > sayHello ( ) ;
Интеллектуальные указатели (современный C++)
В современном программировании на C++ Стандартная библиотека содержит смарт-указатели, которые позволяют гарантировать, что программы свободны от памяти, утечки ресурсов и являются надежными.
Использование интеллектуальных указателей
Смарт-указатели определяются в std пространстве имен в файле заголовка std . Они крайне важны для RAII или получения ресурса — это идиома программирования инициализации. Главная задача этой идиомы — обеспечить, чтобы одновременно с получением ресурса производилась инициализация объекта, чтобы все ресурсы для объекта создавались и подготавливались в одной строке кода. На практике основным принципом RAII является предоставление владения любым ресурсом, выделенным в куче — , например динамически выделяемой памяти или дескрипторами системных объектов — для выделенного стека объекта, деструктор которого содержит код для удаления или освобождения ресурса, а также любой связанный код очистки.
В большинстве случаев при инициализации необработанного указателя или дескриптора ресурса для указания на фактический ресурс следует сразу же передать указатель в интеллектуальный указатель. В современном C++ необработанные указатели используются только в небольших блоках кода с ограниченной областью, циклах или вспомогательных функциях, когда важна производительность и вероятность проблем с владением низкая.
В следующем примере сравниваются объявления необработанного и интеллектуального указателей.
Как показано в примере, интеллектуальный указатель — это шаблон класса, который объявляется в стеке и инициализируется с помощью необработанного указателя, указывающего на размещенный в куче объект. После инициализации интеллектуальный указатель становится владельцем необработанного указателя. Это означает, что интеллектуальный указатель отвечает за удаление памяти, заданной необработанным указателем. Деструктор интеллектуального указателя содержит вызов для удаления, и поскольку интеллектуальный указатель объявлен в стеке, его деструктор вызывается, как только интеллектуальный указатель оказывается вне области, даже если исключение создается где-либо в другой части стека.
Доступ к инкапсулированному указателю осуществляется с помощью знакомых операторов указателя -> и * , которые класс интеллектуального указателя перегружает для возврата инкапсулированного необработанного указателя.
Этот интеллектуальный указатель C++ напоминает создание объектов в таких языках, как C#: вы создаете объект, а система удаляет его в правильный момент. Отличие заключается в том, что отсутствует отдельный сборщик мусора, работающий в фоновом режиме; память управляется через стандартные правила области C++, чтобы среда выполнения функционировала быстрее и эффективнее.
Всегда создавайте интеллектуальные указатели в отдельной строке кода; ни в коем случае не делайте это в списке параметров, чтобы не произошла небольшая утечка ресурсов, связанная с определенными правилами выделения памяти спискам параметров.
В следующем примере показано, как unique_ptr тип интеллектуального указателя из стандартной библиотеки C++ можно использовать для инкапсуляции указателя на большой объект.
В этом примере показаны следующие важные шаги, необходимые для использования интеллектуальных указателей.
Объявите интеллектуальный указатель как автоматическую (локальную) переменную. (Не используйте new malloc выражение или для самого интеллектуального указателя.)
В параметре типа укажите тип, на который указывает инкапсулированный указатель.
Передайте необработанный указатель на new объект-ED в конструкторе интеллектуального указателя. (Некоторые служебные функции или конструкторы интеллектуальных указателей делают это автоматически.)
Используйте перегруженные операторы -> и * для доступа к объекту.
Интеллектуальный указатель удаляет объект автоматически.
Интеллектуальные указатели разработаны для обеспечения максимальной эффективности в отношении памяти и производительности. Например, единственный элемент данных в unique_ptr — это инкапсулированный указатель. Это означает, что размер unique_ptr точно такой же, как и у указателя — 4 или 8 байтов. Доступ к инкапсулированному указателю с помощью перегрузки смарт-указателя * и- > Operators значительно медленнее, чем доступ к необработанным указателям напрямую.
Интеллектуальные указатели имеют собственные функции-члены, доступ к которым осуществляется с помощью нотации «точка». Например, некоторые интеллектуальные указатели стандартной библиотеки C++ имеют функцию-член reset, которая освобождает владение указателем. Это полезно, когда нужно освободить память, принадлежащую интеллектуальному указателю, не дожидаясь, пока интеллектуальный указатель окажется вне области, как показано в следующем примере.
Смарт-указатели обычно предоставляют способ прямого доступа к необработанному указателю. Интеллектуальные указатели стандартной библиотеки C++ имеют get функцию-член для этой цели и CComPtr имеют открытый p член класса. Предоставляя прямой доступ к базовому указателю, можно использовать интеллектуальный указатель для управления памятью в своем коде и по-прежнему передавать необработанный указатель коду, который не поддерживает интеллектуальные указатели.
Виды смарт-указателей
В следующем разделе приведены различные виды интеллектуальных указателей, доступные в среде программирования Windows, и приводится описание их использования.
Интеллектуальные указатели стандартной библиотеки C++
Используйте эти интеллектуальные указатели как основной вариант для инкапсуляции указателей на простые старые объекты C++ (POCO).
unique_ptr
Обеспечивает, чтобы у базового указателя был только один владелец. Используйте как вариант по умолчанию для POCO, кроме случая, когда вы точно знаете, что требуется shared_ptr . Может быть передан новому владельцу, но не может быть скопирован или сделан общим. Заменяет auto_ptr , использовать который не рекомендуется. Сравните с boost::scoped_ptr . unique_ptr является небольшим и эффективным; Размер — один указатель, который поддерживает ссылки rvalue для быстрой вставки и извлечения из коллекций стандартных библиотек C++. Файл заголовка: <memory> . Дополнительные сведения см. в разделе инструкции. Создание и использование экземпляров unique_ptr и unique_ptr класса.
shared_ptr
Интеллектуальный указатель с подсчитанными ссылками. Используйте, когда необходимо присвоить один необработанный указатель нескольким владельцам, например, когда копия указателя возвращается из контейнера, но требуется сохранить оригинал. Необработанный указатель не будет удален до тех пор, пока все владельцы shared_ptr не выйдут из области или не откажутся от владения. Размер — 2 указателя; один — для объекта и второй — для блока общего элемента управления, который содержит счетчик ссылок. Файл заголовка: <memory> . Дополнительные сведения см. в разделе инструкции. Создание и использование экземпляров shared_ptr и shared_ptr класса.
weak_ptr
Интеллектуальный указатель для особых случаев использования с shared_ptr . weak_ptr предоставляет доступ к объекту, который принадлежит одному или нескольким экземплярам shared_ptr , но не участвует в подсчете ссылок. Используйте, когда требуется отслеживать объект, но не требуется, чтобы он оставался в активном состоянии. Требуется в некоторых случаях для разрыва циклических ссылок между экземплярами shared_ptr . Файл заголовка: <memory> . Дополнительные сведения см. в разделе инструкции. Создание и использование экземпляров weak_ptr и weak_ptr класса.
интеллектуальные указатели для COM-объектов (классическое программирование Windows)
При работе с COM-объектами создайте оболочку для указателей интерфейса в соответствующем типе интеллектуальных указателей. Библиотека шаблонных классов (ATL) определяет несколько интеллектуальных указателей для различных целей. Можно также использовать тип интеллектуального указателя _com_ptr_t , который компилятор использует при создании классов оболочки из файлов с расширением TLB. Это лучший вариант, если вы не хотите включать файлы заголовков ATL.
Класс CComPtr
Используйте, если невозможно использовать ATL. Выполняет подсчет ссылок с помощью методов AddRef и Release . Дополнительные сведения см. в разделе инструкции. Создание и использование экземпляров CComPtr и CComQIPtr.
Класс CComQIPtr
Похож на CComPtr , но также предоставляет упрощенный синтаксис для вызова QueryInterface COM-объекта. Дополнительные сведения см. в разделе инструкции. Создание и использование экземпляров CComPtr и CComQIPtr.
Класс Ккомхеапптр
Интеллектуальный указатель на объекты, которые используют CoTaskMemFree для освобождения памяти.
Класс Ккомгитптр
Интеллектуальный указатель для интерфейсов, получаемых из глобальной таблицы интерфейсов (GIT).
Класс _com_ptr_t
По функциональности аналогичен CComQIPtr , но не зависит от заголовков ATL.
Интеллектуальные указатели ATL для объектов POCO
Помимо смарт-указателей для COM-объектов, ATL также определяет смарт-указатели и коллекции смарт-указателей для простых старых объектов C++ (POCO). в классическом Windows программировании эти типы являются полезными альтернативами для коллекций стандартной библиотеки c++, особенно если переносимость кода не требуется или если не требуется смешивать модели программирования стандартной библиотеки c++ и ATL.
Класс Каутоптр
Интеллектуальный указатель, принудительно реализующий уникальное владение путем переноса владения на копию. Сравним с нерекомендуемым классом std::auto_ptr .
Класс Чеапптр
Интеллектуальный указатель для объектов, которые выделены с помощью функции malloc C.
Класс Каутовекторптр
Интеллектуальный указатель для массивов, память для которых выделяется с помощью new[] .
Класс Каутоптраррай
Класс, инкапсулирующий массив элементов CAutoPtr .
Класс Каутоптрлист
Класс, инкапсулирующий методы для управления списком узлов CAutoPtr .