С. Мейерс - Эффективный и современный C++ (1114942), страница 31
Текст из файла (страница 31)
И нтеллектуальные указатели4.2 . Испопьзуйте s td : : sharedytr дпя управпенияресурсами путем совместноrо впаденияПрограммисты на языках программирования со сборкой мусора показывают пальцами на программистов на С++ и смеются над ними, потому что те озабочены предотвращением утечек ресурсов. "Какой примитив! - издеваются они.
- Вы что, застряли в 1 960-х годах и в Lisp? Управлять временем жизни ресурсов должны машины, а нелюди': Разработчики на С++ не остаются в долгу: "Вы имеете в виду, что единственнымресурсом является память, а время освобождения ресурса должно быть принципиальнонеопределимо? Спасибо, мы предпочитаем обобщенность и предсказуемость деструкторов!" Впрочем, в нашей браваде есть немного бахвальства. Сборка мусора в действительности достаточно удобна, а управление временем жизни вручную действительно можетпоказаться похожим на создание микросхем памяти из шкуры медведя с помощью каменных ножей. Почему же не получается взять лучшее из двух миров - систему, котораяработает автоматически (как сборка мусора) и к тому же применима ко всем ресурсами имеет предсказуемое время выполнения (подобно деструкторам)?Интеллектуальный указатель s t d : : shared_pt r представляет собой способ, которым С++ 1 1 объединяет эти два мира.
Объект, доступ к которому осуществляется через указатели std : : sha red_ptr, имеет время жизни, управление которым осуществляется этими указателями посредством совместного вла дения. Никакой конкретныйуказатель std : : shared_pt r не владеет данным объектом. Вместо этого все указателиstd : : shared_pt r, указывающие на него, сотрудничают для обеспечения гарантии, чтоего уничтожение произойдет в точке, где он станет более ненужным. Когда последнийуказатель s t d : : shared_pt r, указывающий на объект, прекратит на него указывать (например, из-за того, что этот st d : : shared_pt r будет уничтожен или перенаправленна другой объект), этот std : : shared_pt r уничтожит объект, на который он указывал.Как и в случае сборки мусора, клиентам не надо самим беспокоиться об управлении временем жизни объектов, на которые они указывают, но, как и при работе с деструкторами, время уничтожения объекта оказывается строго определенным.Указатель s t d : : shared_ptr может сообщить, является ли он последним указателем,указывающим на ресурс, с помощью счетчика ссылок, значения, связанного с ресурсоми отслеживающего, какое количество указателей std : : shared_pt r указывает на него.Конструкторы std : : shared_pt r увеличивают этот счетчик (обычно увеличивают - см.ниже), деструкторы std : : shared_pt r уменьшают его, а операторы копирующего присваивания делают и то, и другое.
(Если spl и sp2 являются указателями std : : shared_ptr,указывающими на разные объекты, присваивание "splsp2 ; " модифицирует spl так,что он указывает на объект, на который указывает sp2. Конечным результатом присваивания является то, что счетчик ссылок для объекта, на который изначально указывалspl, уменьшается, а значение счетчика для объекта, на который указывает sp2, увеличивается.) Если std : : shared_pt r после выполнения декремента видит нулевой счетчикссылок, это означает, что на ресурс не указывает больше ни один std : : shared_pt r, такчто наш интеллектуальный указатель освобождает этот ресурс.=4.2.
Используйте std::sha red_ptr для управления ресурсами путем совместноrо владения1 33Наличие счетчиков ссылок влияет на производительность.•Размер std : : shared_y tr в два раза больше размера обычноrо указателя, поскольку данный интеллектуальный указатель содержит обычный указатель на ресурс и другой обычный указатель на счетчик ссылок2••Память для счетчика ссылок должна выделяться динамически.
Концептуально счетчик ссылок связан с объектом, на который он указывает, однако самуказываемый объект об этом счетчике ничего не знает. В нем нет места для хранения счетчика ссылок. (Приятным следствием этого является то, что интеллектуальный указатель s t d : : shared_pt r может работать с объектами любого типа(в том числе встроенных типов).) В разделе 4.4 поясняется, что можно избежатьстоимости динами•1еского выделения при создании указателя std : : shared_pt rс помощью вызова std : : make_shared, однако имеются ситуации, когда функцияs t d : : make _ shared неприменима.
В любом случае счетчик ссылок хранится в динамически выделенной памяти.•посколькумогут присутствовать одновременное чтение и запись в разных потоках. Например, s t d : : shared_ptr, указывающий на ресурс в одном потоке, может выполнятьсвой деструктор (тем самым уменьшая количество ссылок на указываемый им ресурс), в то время как в другом потоке указатель std : : shared_ptr на тот же объект может быть скопирован (а следовательно, увеличивает тот же счетчик ссылок).Атомарные операции обычно медленнее неатомарных, так что несмотря на то, чтообычно счетчики ссылок имеют размер в одно слово, следует рассматривать ихчтение и запись как относительно дорогостоящие операции.Инкремент и декремент счетчика ссылок должны быть атомарными,Возбудил ли я ваше любопытство, когда написал, что конструкторы std : : shared_p t r "обычно" увеличивают счетчик ссылок для указываемого объекта? Присозданииstd : : shared_pt r, указывающего на объект, всегда добавляется еще один интеллектуальный указатель std : : sha red_ptr, так почему же счетчик ссылок может увеличиваться не всегда?Из-за перемещающего конструирования - вот почему.
Перемещающее конструирование указателя s t d : : shared_pt r из другого s t d : : shared_pt r делает исходный указатель нулевым, а это означает, что старый s t d : : shared_ptr перестает указывать на ресурс в тот же момент, в который новый std : : shared_ptr начинает это делать. В результате изменение значения счетчика ссылок не требуется. Таким образом, перемещениеstd : : sha red_pt r оказывается быстрее копирования: копирование требует увеличениясчетчика ссылок, а перемещение - нет. Это справедливо как для присваивания, таки для конструирования, так что перемещающее конструирование быстрее копирующегоконструирования, а перемещающее присваивание быстрее копирующего присваивания.2 Стандарт не требует испопьзования именно такой реапизации, но все известные мне реапизациистандартной бибпиотеки ноступают именно так.1 34Глава 4.
Интеллектуальные указателиПодобно std : : unique_pt r (см. раздел 4. l ), std : : shared_pt r в качестве механизмаудаления ресурса по умолчанию использует delete, но поддерживает и пользовательскиеудалители. Однако дизайн этой поддержки отличается от дизайна для std : : un i que_ptr.Для s t d : : unique_pt r тип удалителя является частью типа интеллектуального указателя.Для std : : shared_ptr это не так:auto loggingDel=[ ] ( Widget *pw ) / / Пользовательский удалитель/ / ( как в разделе 4 .
1 ){makeLogEntry ( pw ) ;delete pw;1;std: : unique_ytr</ / Тип удалителя являетсяWidget, decltype (logqinqDel) // частью типа указателя> upw ( new Widget, loggingDel ) ;/ / Тип удалителя не являетсяstd: : shared_ytr<Widget>spw ( new Widget , loggingDe l ) ; / / частью типа указателяДизайн std : : shared_pt r более гибок. Рассмотрим два указателя std : : shared_pt r<Widget >, каждый со своим пользовательским удалителем разных типов (например, из-затого, что пользовательские удалители определены с помощью лямбда-выражений):auto customDeleterlauto customDeleter2[][](Widget *pw)( Widget *pw)f ; 1 1 Пользовательскиеf ; 1 1 удалители1 1 разных типовstd : : shared_ptr<Widget> pwl ( new Widget , customDeleterl ) ;std : : shared_ptr<Widget> pw2 ( new Widget , customDeleter2 ) ;Поскольку pwl и pw2 имеют один и тот же тип, они могут быть помещены в контейнеробъектов этого типа:std : : vector<std : : shared_ptr<Widge t>> vpw { pwl , pw2 f ;Они также могут быть присвоены один другому и переданы функции, принимающейпараметр типа s t d : : shared_ptr<Widget>.
Ни одно из этих действий не может быть выполнено с указателями std : : uni que_ptr, которые отличаются типами пользовательскихудалителей, так как эти типы влияют на тип самого std : : uni que_ptr.Друтим отличием от s t d : : uni que_pt r является то, что указание пользовательскогоудалителя не влияет на размер объекта s t d : : shared_pt r.
Независимо от удалителя объектstd : : shared_pt r имеет размер, равный размеру двух указателей. Это хорошая новость, ноона должна привести вас в замешательство. Пользовательские удалители могут быть функциональными объектами, а функциональные объекты могут содержать любое количестводанных, а значит, быть любого размера. Как же std : : shared_pt r может обращаться к удалителю произвольного размера, не используя при этом дополнительной памяти?4.2.Используйте std::shared_ptr дnя управления ресурсами путем совместного впадения1 35А он и не может.
Ему приходится использовать дополнительную память, но эта память не является частью объекта s t d : : sha red_pt r. Она располагается в динамическойпамяти или, если создатель s t d : : shared_pt r воспользуется поддержкой со стороныs t d : : shared_p t r пользовательских распределителей памяти, там, где выделит памятьтакой распределитель.
Ранее я отмечал, что объект std : : shared_ptr содержит указательна счетчик ссылок для объекта, на который он указывает. Это так, но это немного нетак, поскольку счетчик ссылок является частью большей структуры данных, известнойпод названием управляющий блок (control Ыосk). Управляющий блок имеется для каждого объекта, управляемого указателями s t d : : shared_ptr. Управляющий блок в дополнение к счетчику ссылок содержит копию пользовательского удалителя, если таковой былуказан.
Если указан пользовательский распределитель памяти, управляющий блок содержит и его копию. Управляющий блок может также содержать дополнительные данные,включающие, как поясняется в разделе 4.4, вторичный счетчик ссылок, известный какслабый счетчик, но в данном разделе мы его игнорируем. Мы можем представить память,связанную с объектом s t d : : shared_ptr<T>, как имеющую следующий вид:std : : shared ptr<T>Указатеnь на тУправляющий блок объекта настраивается функцией, создающей первый указательstd : : shared_ptr на объект. Как минимум это то, что должно быть сделано.
В общемслучае функция, создающая указатель std : : shared ptr на некоторый объект, не можетзнать, не указывает ли на этот объект некоторый другой указатель s t d : : shared_pt r, такчто при создании управляющего блока должны использоваться следующие правила.•Функция std : : make_shared (см. раздел 4.4) всегда создает управляющий блок.Она производит новый объект, на который будет указывать интеллектуальныйуказатель, так что в момент вызова std : : make_shared управляющий блок для этого объекта, определенно, не существует.•Управляющий блок создается тогда, когда указатель std : : shared_ptr создается из указателя с исключительным владением (т.е. std : : unique_ptr илиstd : : auto_ptr).