С. Мейерс - Эффективный и современный C++ (1114942), страница 32
Текст из файла (страница 32)
Указатели с исключительным владением не используют управляющие блоки, так что н икакого управляющего блока для указываемого объекта несуществует. (Как часть своего построения std : : sha red_pt r осуществляет владение указываемым объектом, так что указатель с исключительным владением становится нулевым.)1 36Гnава 4. Интеnnектуаnьные указатеnи•Когда конструктор std : : sharedytr вызывается с обычным указателем, онЕсли вы хотите создать std : : shared_ptr из объекта, у которого уже имеется управляющий блок, вы предположительно передаетев качестве аргумента конструктора std : : shared_pt r или std : : weak_ptr (см. раздел 4.3), а не обычный указатель. Конструкторы std : : shared_pt r, принимающиев качестве аргументов указатели std : : shared_ptr или std : : weak_pt r, не создаютновые управляющие блоки, поскольку могут воспользоваться управляющими блоками, на которые указывают переданные им и нтеллектуальные указатели.создает управляющий блок.Следствием из этих правил является то, что создание более одного std : : shared_ptrиз единственного обычного указателя дает вам бесплатный билет для путешествия в неопределенное поведение, поскольку в результате указываемый объект будет иметь несколько управляющих блоков.
Несколько управляющих блоков означают несколько счетчиков ссылок, а несколько счетчиков ссылок означают, что объект будет удален несколько раз (по одному для каждого счетчика ссылок). И все это значит, что приведенныйниже код плох, ужасен, кошмарен:auto pw=new Widget ;11 pw-обычный указательstd : : shared_ptr<Widget>spwl (pw loggingDe l ) ; / / Создание управляющего блока для *pw,std: : shared_ptr<Widget>/ / Создание второгоspw2 (pw, loggingDe l ) ; / / управляющего блока для *pw !Создание обычного указателя pw, указывающего н а динамически выделенный объект, - плохое решение, поскольку оно противоречит главному совету всей главы: предпочитайте интеллектуальные указатели обычным указателям. (Если вы забыли, откудавзялся этот совет, заново прочтите первую страницу данной главы.) Но пока что забудемоб этом.
Строка, в которой создается pw, представляет собой стилистическую мерзость,но она по крайней мере не приводит к неопределенному поведению.Далее вызывается конструктор для spwl, которому передается обычный указатель, такчто этот конструктор создает управляющий блок (и тем самым счетчик ссылок) для того,на что он указывает. В данном случае это *pw (т.е. объект, на который указывает pw).Само по себе это не является чем-то страшным, но далее с тем же обычным указателемвызывается конструктор для spw2, и он также создает управляющий блок (а следовательно, и счетчик ссылок) для *pw. Объект *pw, таким образом, имеет два счетчика ссылок,каждый из которых в конечном итоге примет нулевое значение, и это обязательно приведет к попытке уничтожить объект *pw дважды.
Второе уничтожение и будет ответственно за неопределенное поведение.Из этого можно вынести как минимум два урока, касающиеся примененияstd : : shared_ptr. Во-первых, пытайтесь избегать передачи обычных указателей конструктору std : : shared ptr. Обычной альтернативой этому является применение функцииstd : : make_shared (см. раздел 4.4), но в примере выше мы использовали пользовательские4.2.Используйте std::shared_ptr дл я управления ресурсами путем совместноrо владения1 37удалители, а это невозможно при использовании std : : make_shared. Во-вторых, если вывынуждены передавать обычный указатель конструктору std : : shared_ptr, передавайтенепосредственно результат оператора new, а не используйте переменную в качестве посредника.
Если первую часть кода переписать следующим образом,s td : : shared_ptr<Widget>spwl (new Widget, / / Непосредственное использование newloggingDel) ;то окажется гораздо труднее создать второй std : : shared_ptr из того же обычного указателя. Вместо этого автор кода, создающего spw2, естественно, будет использовать в качестве аргумента инициализации spwl (т.е. будет вызывать копирующий конструкторs t d : : shared_pt r), и это не будет вести ни к каким проблемам:std : : shared_ptr<Widget>spw2 ( spwl ) ;11 spw2 использует тот же/ / управляющий блок, что и spwlОсобенно удивительный способ, которым применение переменных с обычными указателями в качестве аргументов конструкторов std : : shared_pt r может привести к множественным управляющим блокам, включает указатель t h i s .
Предположим, что нашапрограмма использует указатели std : : shared_pt r для управления объектами Widget и унас есть структура данных, которая отслеживает объекты Widget, которые были обработаны:std : : vector<std: : shared_ytr<Widget>> p roce ss edWidget s ;Далее, предположим, что Widget имеет функцию-член, выполняющую эту обработку:class WidgetpuЫic :void process ( ) ;};Вот вполне разумно выглядящий подход к написанию Widget : : process:void Widget : : process ( ){1 1 Обработка WidgetprocessedWidgets . emplace_back (this) ; / / Добавление в список/ / обработанных Widge t ;1 1 это неправильно !Комментарий о неправильности кода относится не к использованию empl ace_back,а к передаче t h i s в качестве аргумента.
(Если вы не знакомы с emp l ace_back, обратитесь к разделу 8.2.) Этот код будет компилироваться, но он передает обычный указатель(this) контейнеру указателей std : : shared_ptr. Конструируемый таким образом указатель std : : shared_pt r будет создавать новый управляющий блок для указываемого1 38Гn ава 4.
Интеnnектуаnьные указатеnиWidget (т.е. для объекта * t h i s). Это не выглядит ужасным до тех пор, пока вы не понимаете, что, если имеются указатели std : : shared_ptr вне функции-члена, которые ужеуказывают на этот объект Widget, вам не избежать неопределенного поведения.API s t d : : s ha r e d _pt r включает средство для ситуаций такого рода. Вероятно, его имя - наиболее странное среди всех имен стандартной библиотеки:std : : еnаЫе shared_from_ t h i s.
Это шаблон базового класса, который вы наследуете,если хотите, чтобы класс, управляемый указателями s t d : : shared_pt r, был способенбезопасно создавать std : : shared_ptr из указателя this. В нашем примере Widget будетунаследован от std : : еnаЫе_shared_ from_this следующим образом:_class Widget : puЬlic std: : enaЫe_shared_from_this<Widget>puЫic :void process ( ) ;};Как я уже говорил, s t d : : еnаЫе shared_ from_t h i s является шаблоном базовогокласса. Его параметром типа всегда является имя производного класса, так что классWidget порождается от std : : enaЬle shared_ from_ this<Widget>. Если идея производного класса, порожденного от базового класса, шаблонизированного производным, вызывает у вас головную боль, попытайтесь об этом не думать.
Этот код совершенно законный, а соответствующий ш аблон проектирования хорошо известен, имеет стандартноеимя, хотя и почти такое же странное, как std : : еnаЫе_shared_ from this. Это имя Странно повторяющийся шаблон ( The Curiously Recurring Teтplate Pattern - CRTP). Есливы хотите узнать о нем побольше, расчехлите свой поисковик, поскольку сейчас мы возвращаемся к нашему барану по имени std : : еnаЫе_shared from t h i s.Шаблон s t d : : enaЬ l e_ shared_ from_ this определяет функцию-член, которая создаетstd : : shared_pt r для текущего объекта, но делает это, не дублируя управляющие блоки.Это функция-член shared_from this, и вы должны использовать ее в функциях-членахтогда, когда вам нужен s t d : : shared_ptr, который указывает на тот же объект, что и указатель this. Вот как выглядит безопасная реализация W idget : : p rocess:_____void Widget : : process ( ){/ / Как и ранее , обработка Widget/ / Добавляем std : : shared_ptr, указывающий на/ / текущий объект, в processedWidgetsprocessedWidgets .
emplace_back ( shared_from_this ( ) ) ;Внутри себя shared_ from_this ищет управляющий блок текущего объекта и создаетновый std: : shared ptr, который использует этот управляющий блок. Дизайн функцииполагается на тот факт, что текущий объект имеет связанный с ним управляющий блок.Чтобы это было так, должен иметься уже существующий указатель std : : sha red_p t r4 .2. Испоnьзуйте std::shared_ptr дnя управnения ресурсами путем совместного впадения1 39(например, за пределами функции-члена, вызывающей shared from this ) , который указывает на текущий объект.
Если такого s t d : : shared_ptr нет (т.е. если текущий объектне имеет связанного с н им управляющего блока), результатом будет неопределенное поведение, хотя обычно shared_ from_this генерирует исключение.Чтобы препятствовать клиентам вызывать функции-члены, в которых используетсяsha red_from_this, до того как на объект будет указывать указатель s t d : : shared_ptr,классы, наследуемые от std : : enaЫe_ shared_ from_t h i s , часто объявляют свои конструкторы как pri vate и заставляют клиентов создавать объекты путем вызова фабричных функций, которые возвращают указатели std : : shared_ptr.
Например, класс Widgetможет выглядеть следующим образом:class Widge t : puЬ l i c std: : enaЬle_shared_ from_this<Widget>puЬlic :11 Фабричная функция, пересылающая11 аргументы закрытому конструктору :teшplate<t:ypename . . . Ts>static std: : shared_ytr<Widget> create (Ts&& . . . params) ;void process ( ) ;11Как и ранее11Конструкторыprivate :1;В настоящее время вы можете только смутно припоминать, что наше обсуждениеуправляющих блоков было мотивировано желанием понять, с какими затратами связаноприменение s t d : : shared_ptr. Теперь, когда мы понимаем, как избегать создания слишком большого количества управляющих блоков, вернемся к нашей первоначальной теме.Управляющий блок обычно имеет размер в несколько слов, хотя пользовательскиеудалители и распределители памяти могут его увеличить. Обычная реализация управляющего блока оказывается более интеллектуальной, чем можно было бы ожидать.