С. Мейерс - Эффективный и современный C++ (1114942), страница 35
Текст из файла (страница 35)
. . Ts>std : : unique_ptr<T> make_unique (Ts&& . . . params )return std: : unique_pt r<T> (new T ( st d : : forward<Ts> (params ) . . . ) ) ;Как вы можете видеть, ma ke_unique просто выполняет прямую передачу своих параметров в конструктор создаваемого объекта, создает s t d : : unique_pt r из обычного указателя, возвращаемого оператором new, и возвращает этот указатель st d : : unique_pt r.Функция в данном виде не поддерживает массивы или пользовательские удалители (см.раздел 4. 1 ), но зато демонстрирует, как с минимальными усилиями можно при необходимости создать make_unique'. Только не помещайте вашу версию в пространство именs t d, поскольку иначе вы можете столкнуться с коллизией имен при обновлении реализации стандартной библиотеки до С++ 14.Функции s t d : : make_unique и s t d : : ma ke_shared представляют собой две из трехmа kе-функций, т.е.
функций, которые принимают произвольное количество аргументов, выполняют их прямую передачу конструктору объекта, создаваемого в динамической памяти, и возвращают интеллектуальный указатель на этот объект. Третья mаkефункция s t d : : a llocate_shared. Она работает почти так же, как и s t d : : ma ke_shared,за исключением того, что первым аргументом является объект распределителя, использующийся для выделения динамической памяти.Даже самое тривиальное сравнение создания интеллектуального указателя с помощью mаkе-функции и без участия таковой показывает первую причину, по которой применение таких функций является предпочтительным. Рассмотрим следующий код.-auto upwl ( std : : make_unique<Widget> ( ) ) ;11 С mаkе - функциейstd : : unique_pt r<Widget> upw2 ( new Widget) ; / / Без mаkе - функцииauto spwl ( std : : make_shared<Widget> ( ) ) ;/! С mаkе -функциейstd : : shared_ptr<Widget> spw2 ( new Widget) ; / / Без mаkе - функцииЯ подчеркнул важное отличие: версия с применением оператора new повторяет созданный тип, в то время как mа kе-функции этого не делают.
Повторение типа идет вразрез с основным принципом разработки программного обеспечения: избегать дублирования кода. Дублирование в исходном тексте увеличивает время компиляции, можетвести к раздутому объектному коду и в общем случае приводит к коду, с которым сложно работать. Зачастую это ведет к несогласованному коду, а несогласованности в кодечасто ведут к ошибкам. Кроме того, чтобы набрать на клавиатуре что-то дважды, надо3Дня создания полнофункциональной версии make unique с минимальными усилиями поищитедокумент, ставший ее источником, и скопируйте из него ее реализацию. Этот документ N3656от 18 апреля 20 1 3 года, его автор - Стивен Т.
Лававей (Stephan Т. Lavavej)._-4.4. Предпочитайте использование std::make_unique и std::make_shared ""1 47затратить в два раза больше усилий, чем для единственного набора, а кто из нас не любитсократить эту работу?Второй причиной для предпочтения mа kе-функций является безопасность исключений. Предположим, что у нас есть функция для обработки Widget в соответствии с некоторым приоритетом:void processWidget ( std: : shared_ptr<Widget> spw, int priori ty ) ;Передача std : : sha red_pt r по значению может выглядеть подозрительно, но в разделе 8.
1 поясняется, что если processWidget всегда делает копию s t d : : shared_ptr (например, сохраняя ее в структуре данных, отслеживающей обработанные объекты Widget ) , тоэто может быть разумным выбором.Предположим теперь, что у нас есть функция для вычисления приоритетаint computePriority ( ) ;и мы используем ее в вызове proces sWidget, который использует оператор new вместоstd : : make shared:proces sWidget (std : : shared_ptr<Widget> ( new Widge t ) ,computePriorit y ( ) ) ;/ / Потенциальная/ / утечка/ / ресурсаКак указывает комментарий, этот код может приводить к утечке Widget, вызваннойприменением new.
Но почему? И вызывающий код, и вызываемая функция используютуказатели s td : : shared_pt r, а s t d : : shared_pt r спроектированы для предотвращенияутечек ресурсов. Они автоматически уничтожают то, на что указывают, когда исчезаетпоследний std : : shared_ptr.
Если все везде используют указатели std : : shared_ptr,о какой утечке может идти речь?Ответ связан с тем, как компиляторы транслируют исходный код в объектный. Вовремя выполнения аргументы функции должны быть вычислены до вызова функции,так что в вызове processWidget до того, как processWidget начнет свою работу, должнопроизойти следующее.•Выражение new W idget должно быть вычислено, т.е. в динамической памяти должен быть создан объект W idget.•Должен быть вызван конструктор s t d : : shared _pt r<W i dget >, отвечающий зауправление указателем, сгенерированным оператором new.•Должна быть вызвана функция computePriority.Компиляторы не обязаны генерировать код, выполняющий перечисленные действияименно в таком порядке.
Выражение new W idget должно быть выполнено до вызоваконструктора std : : shared_pt r, поскольку результат этого оператора new используетсяв качестве аргумента конструктора, но функция computePriori ty может быть выполненадо указанных вызовов, после них или, что критично, между ними, т.е. компиляторы могут генерировать код для выполнения операций в следующем порядке.1 48Глава 4 . Интеллектуальные указатели1 .
Выполнить new Widget.2. Выполнить computePriority.3. Вызвать конструктор std : : shared_pt r.Если сгенерирован такой код и во время выполнения computePriority генерирует исключение, созданный на первом шаге в динамической памяти Widget будет потерян, таккак он не будет сохранен в указателе s t d : : shared_pt r, который, как предполагается,начнет управлять им на третьем шаге.Применение std : : ma ke_shared позволяет избежать таких проблем. Вызывающий кодимеет следующий вид:processWidget ( std: : make_shared<Widget> ( ) , / / Потенциальнойcompute Priority ( ) ) ;1 1 утечки ресурсов нетВо время выполнения либо s t d : : ma ke s ha red, либо compu t e P r i o r i t y будет вызвана первой. Если это s t d : : ma ke_sha red, обычный указатель на созданный в динамической памяти W idget будет безопасно сохранен в возвращаемом указателеstd : : shared_pt r до того, как будет вызвана функция compute P ri o r i t y.
Если послеэтого функция computePriority сгенерирует исключение, деструктор std : : sha red_ptrуничтожит объект W idget, которым владеет. А если первой будет вызвана функцияcomputePriority и сгенерирует при этом исключение, то std: : ma ke_shared даже не будет вызвана, так что не будет создан объект Widget, и беспокоиться будет не о чем.Если мы заменим std : : shared_pt r и std : : make_shared указателем std : : unique_ptrи функцией std: : make_unique, все приведенные рассуждения останутся в силе.
Использование std : : make unique вместо new так же важно для написания безопасного с точкизрения исключений кода, как и применение s t d : : ma ke_ shared.Особенностью std : : make_ shared (по сравнению с непосредственным использованием new) является повышенная эффективность.
Применение std: : make_shared позволяеткомпиляторам генерировать меньший по размеру и более быстрый код, использующийболее компактные структуры данных. Рассмотрим следующее непосредственное применение оператора new:std: : shared_ptr<Widget> spw ( new Widget ) ;Очевидно, что этот код предполагает выделение памяти, но фактически он выполняетдва выделения. В разделе 4.2 поясняется, что каждый указатель std : : shared_pt r указывает на управляющий блок, содержащий, среди прочего, значение счетчика ссылокдля указываемого объекта. Память для этого блока управления выделяется в конструкторе std : : shared_pt r. Непосредственное применение оператора new, таким образом, требует одного выделения памяти для Widget и второго - для управляющего блока.Если вместо этого использовать std: : ma ke shared,auto spw=std : : ma ke_shared<Widget> ( ) ;то окажется достаточно одного в ыделения памяти.
Дело в том, что функцияs t d : : ma ke _ shared выделяет один блок памяти для хранения как объекта Widget, так4.4. Предпочитайте испоnьзование std::make_unique и std::make_shared....1 49и управляющего блока. Такая оптимизация снижает статический размер программы, поскольку код содержит только один вызов распределения памяти и повышает скоростьработы выполнимого кода, так как выполняется только одно выделение памяти. Крометого, применение std : : make_shared устраняет необходимость в некоторой учетной информации в управляющем блоке, потенциально уменьшая общий объем памяти, требующийся для программы.Анализ эффективности функции s t d : : ma ke_shared в равной мере применим и кstd : : а l loca t e shared, так что преимущество повышения производительности функции std : : ma ke_shared распространяется и на нее.Аргументы в пользу предпочтения mаkе-функций непосредственному использованиюоператора new весьма существенны.
Тем не менее, несмотря на их проектные преимущества, безопасность исключений и повышенную производительность, данный разделговорит о пред почтительном применении mа kе-функций, но не об их исключительномиспользовании. Дело в том, что существуют ситуации, когда эти функции не могут илине должны использоваться.Например, ни одна из mаkе-функций не позволяет указывать пользовательскиеудалители (см. разделы 4.