С. Мейерс - Эффективный и современный C++ (1114942), страница 36
Текст из файла (страница 36)
1 и 4.2), в то время как конструкторы s t d : : uni que _pt rи std : : shared_pt r это позволяют. Для данного пользовательского удалителя Widgetauto widgetDeleter=[ ] ( Widget* pw) {_);создание интеллектуального указателя с применением оператора new является оченьпростым:std : : unique ptr<Widget, decltype (widgetDeleter) >upw (new Widget, widgetDeleter) ;std : : shared_ptr<Widget> spw (new Widget, widgetDeleter ) ;Сделать то же самое с помощью mаkе-функции невозможно.Второе ограничение на mаkе-функции проистекает из синтаксических деталей ихреализации. В разделе 3 .
1 поясняется, что при создании объекта, тип которого перегружает конструкторы как с параметрами s t d : : i n i t i a l i ze r_l i s t , так и без них,создание объекта с использованием фигурных скобок предпочитает конструкторs td : : i n i t i a l i zer_l i s t , в то время как создание объекта с использованием круглыхскобок вызывает конструктор, у которого нет параметров std : : ini t i a l i zer_ l i st. mаkефункции выполняют прямую передачу своих параметров конструктору объекта, но делается ли это с помощью круглых или фигурных скобок? Для некоторых типов ответна этот вопрос очень важен. Например, в вызовахauto upvauto spvstd : :make_unique<std : : vector<int>> ( l O, 2 0 ) ;std : :make_shared<std: : vector<int>> ( l O, 2 0 ) ;результирующие интеллектуальные указатели должны указывать на векторы std : : vectorс 10 элементами, значение каждого из которых - 20, или на векторы с двумя элементами,один со значением 10, а второй со значением 20? Или результат непредсказуем?1 50Глава 4.
Интеллектуальные указателиХорошая новость в том, что результат все же предсказуем: оба вызова создают векторы std : : vector с 10 элементами, значение каждого из которых равно 20. Это означаетчто в mаkе-функциях прямая передача использует круглые, а не фигурные скобки. Плохаяновость в том, что если вы хотите создавать свои указываемые объекты с помощью инициализаторов в фигурных скобках, то должны использовать оператор new непосредственно.
Использование mаkе-функции требует способности прямой передачи инициализаторав фигурных скобках, но, как поясняется в разделе 5.8, такие инициализаторы не могут бытьпереданы прямо. Однако в разделе 5.8 описывается и обходной путь: использование выводатипа auto для создания объекта std : : ini t ia l i zer_ l is t из инициализатора в фигурныхскобках (см.
раздел 1 .2) с последующей передачей созданного объекта через mаkе-функцию:11Создание std: : initiali zer_listauto initList = { 10 , 20 } ;11Создание std : : vector с помощью конструкторас параметром std : : initial i zer_l istauto spvstd : : ma ke_shared<std : : vector<int» ( initList) ;11=Для std : : unique _p t r эти два сценария (пользовательские удалители и фигурныеинициализаторы) являются единственными, когда применение mаkе-функции оказывается проблематичным.
Что касается s t d : : shared_pt r и его mаkе-функций, то есть еще двасценария. Оба они являются крайними случаями, но некоторые разработчики постоянноходят по краю, и вы можете быть одним из них.Некоторые классы определяют собственные версии operator new и operator de lete.Наличие этих функций подразумевает, что глобальные функции выделения и освобождения памяти для объектов этого типа являются неприемлемыми. Зачастую подпрограммы для конкретных классов разрабатываются только для выделения и освобожденияблоков памяти, по размеру точно совпадающих с размером объектов класса; например,ope rator new и operator delete для класса Widget зачатую способны выделять и освобождать только блоки памяти размером s i zeof ( W i dget ) .
Такие подпрограммы плохо подходят для поддержки пользовательского распределения памяти для указателейstd : : shared _ptr (с помощью s t d : : a l locate _shared) и их удаления (с помощью пользовательских удалителей), поскольку количество запрашиваемой s t d : : а llocate_sharedпамяти не совпадает с размером динамически создаваемого объекта (который равенразмеру этого объекта плюс размер управляющего блока). Соответственно, применениеmаkе-функций для создания объектов типов со специфичными для данного класса версиями operator new и operator delete обычно является плохой идеей.Преимущества размера и скорости функции s t d : : ma ke_shared по сравнению с непосредственным применением оператора new вытекают из того факта, что управляющийблок указателя std : : shared_ptr размещается в том же блоке памяти, что и управляемыйобъект.
Когда счетчик ссылок объекта становится равным нулю, объект уничтожается(т.е. вызывается его деструктор). Однако занятая им память не может быть освобожденадо тех пор, пока не будет уничтожен и управляющий блок, поскольку блок динамическивыделенной памяти содержит как объект, так и управляющий блок.4.4. П редпочитайте использование std::make_uпique и std::make_shared....151Как я уже отмечал, управляющий блок содержит, помимо самого счетчика ссылок,некоторую учетную информацию. Счетчик ссылок отслеживает, сколько указателейs t d : : shared_pt r ссылаются на управляющий блок, но управляющий блок содержити второй счетчик ссылок, который подсчитывает, сколько указателей std : : weak_ptrссылаются на этот управляющий блок. Этот второй счетчик ссылок известен как слабыйсчетчик ( weak couпt)4• Когда указатель std : : weak_pt r проверяет, не является ли он просроченным (см.
раздел 4.2), он делает это путем обращения к счетчику ссылок (но нек слабому счетчику) в управляющем блоке, на который ссылается. Если счетчик ссылокравен нулю (т.е. если указываемый объект не имеет указателей std : : shared_ptr, указывающих на него, и, таким образом, является удаленным), указатель std : : weak_ptr является просроченным. В противном случае он просроченным не является.Пока указатели std : : wea k_pt r указывают на управляющий блок (т.е. слабый счетчик больше нуля), этот управляющий блок должен продолжать существовать. А покасуществует управляющий блок, память, его содержащая, должна оставаться выделенной.
Таким образом, память, выделенная mаkе-функцией для std : : shared_pt r не может быть освобождена до тех пор, пока не будут уничтожены последний указательstd : : shared_ptr и последний указатель std : : weak_ptr, ссылающиеся на объект.Если время между уничтожением последнего s t d : : s ha re d_pt r и последнегоs t d : : weak_pt r значительно, между уничтожением объекта и занимаемой им памятьюможет происходить задержка, что особенно важно для типов с большим размером:class ReallyBigType {auto pBigObj...1;=11 Создание большогоstd: :make_shared<ReallyBigType> ( ) ; / / объекта с помощью/ / s t d : : make shared/ / Создание указателей std: : shared_ptr и s t d : : weak_ptr// на большой объект и работа с ними/ / Уничтожение последнего указателя std : : shared_ptr на/ / этот объект; остаются указатели s t d : : weak_ptr/ / Во время этого периода память , ранее занятая большим11 объектом, остается занятой11 Уничтожение последнего указателя s td : : wea k_ptr на/ / объект; освобождение памяти, выделенной для/ / управляющего блока и объекта4На практике значение слабого счетчика не всегда совпадает с количеством указателей std : :weak pt r, ссылающихся на управляющий блок, поскольку разработчики библиотеки нашли способы добавлять о слабый счетчик дополнительную информацию, которая упрощает генерацию лучшегокода.
В данном разделе мы игнорируем этот факт и считаем, что значение слабого счетчика представляет собой количество указателей std : : weak_ptr, ссынающихся на управняющий блок.j).1 52Глава 4. И нтеллектуальные указателиПри непосредственном использовании пеw память для объекта Rea l l yBigType можетбыть освобождена, как только уничтожается последний указатель s t d : : shared_ptr, указывающий на него:class ReallyBigType {11 Как и ранее};/ / Создание очень большого объекта с помощью пew : s t d : : sharedptr<Real lyBigType> pBigObj (new ReallyBigType ) ;...1 1 Как и ранее, создание указателей std : : shared_ptr и11 std : : weak_ptr на объект и работа с ними11 Уничтожение последнего указателя std: : shared ptr на11 этот объект; остаются указатели std : : weak_ptr11 Память , выделенная для объекта, освобождается/ / Во время этого периода остается занятой только память ,/ / ранее выделенная для управляющего блока11 Уничтожение последнего указателя std : : weak_ptr на11 объект; освобождение памяти , выделенной для11 управляющего блокаОказавшись в ситуации, когда использование функции std : : make_shared невозможноили неприемлемо, вы можете захотеть защититься от ранее рассматривавшихся намипроблем, связанных с безопасностью исключений.
Лучший способ сделать это - обеспечить немедленную передачу результата операции пеw конструктору интеллектуального указателя в инструкции, которая не делает ничего иного. Это предотвратит созданиекомпилятором кода, который может генерировать исключение между оператором пеwи вызовом конструктора интеллектуального указателя, который будет управлять объектом, созданным оператором new.В качестве примера рассмотрим небольшое изменение небезопасного с точки зренияисключений вызова функции p rocesswidget, которую мы рассматривали ранее. В этотраз мы укажем пользовательский удалитель:void processwidget ( s td : : shared_ptr<Widget> spw, / / Как и ранееiпt priorit y ) ;void cusDel (Widget •ptr ) ;1 1 Пользовательский удалительВот небезопасный с точки зрения исключений вызов:processWidget (11std : : shared_ptr<Widget> ( пew Widge t , cusDel ) , / /computePriority ( )11);11Как и ранее,потенциальнаяутечкаресурса !4.4.
П редпочитайте испоnьэование std::make_unique и std::make_shared""1 53Вспомним: если computePriority вызывается после new W idget, но до конструктораstd : : shared_ptr и если compute Priority генерирует исключение, происходит утечкадинамически созданного Widget.Здесь применение пользовательского удалителя препятствует использованиюstd : : ma ke_shared, так что избежать проблемы можно, поместив создание Widget в динамической памяти и конструирование std : : shared_pt r в собственную инструкцию,а затем вызвав функцию processWidget с передачей ей полученного std : : shared ptr.Вот и вся суть этого метода, хотя, как мы вскоре увидим, его можно подкорректироватьдля повышения производительности:std : : shared_ptr<Widget> spw ( new Widget , cusDel ) ;processWidget ( spw, computePriorit y ( ) ) ; / / Корректно, но не/ / оптимально ; см.
нижеЭтот код работает, поскольку std : : shared_ptr предполагает владение обычным указателем, переданным конструктору, даже если этот конструктор генерирует исключение.В данном примере, если конструктор spw генерирует исключение (например, из-за невозможности выделить динамическую память для управляющего блока), он все равно гарантирует вызов cusDel для указателя, полученного в результате операции new Widget.Небольшое снижение производительности возникает из-за того, что в небезопасномс точки зрения исключений коде мы передавали функции processWidget rvalueprocessWidget (std: : shared_ptr<Widget> (new Widget, cusDel) , 1 1 rvaluecornputePriori ty ( ));а в случае безопасного вызова - передаем lvalue:processWidget ( spw, computePriori ty ( ) ) ;1 1 lvalueПоскольку параметр std : : shared ptr функции processWidget передается по значен ию, создание из rvalue влечет за собой только перемещение, в то время как созданиеиз lvalue требует копирования.