Использование умных указателей в C++

Этот пост - перевод статьи Герба Саттера: GotW #89 Solution: Smart Pointers.

Постановка проблемы

Простой вопрос

1. В каких случаях следует использовать shared_ptr а в каких - unique_ptr? Приведите как можно больше соображенией на эту тему.

Вопросы посложнее

2. Почему практически всегда следует использовать make_shared для создания объекта, которым будут впоследствии владеть shared_ptr? Объясните.

3. Почему практически всегда следует использовать make_unique для создания объекта, которым будет владеть unique_ptr? Объясните.

4. Что такое auto_ptr?

Решение

1. В каких случаях следует использовать shared_ptr а в каких - unique_ptr? Приведите как можно больше соображенией на эту тему.

При сомнении воздерживайся используйте unique_ptr, который всегда можно позднее преобразовать в shared_ptr с помощью перемещения (move-convert), если потребуется. При этом, если изначально известно, что совместное владение объектом будет необходимо, следует сразу использовать shared_ptr, созданный с помощью make_shared (смотри вопрос №2).
Существует три главные причины, чтобы говорить: "при сомнении используйте unique_ptr".
Во-первых, используйте наиболее простые инструменты из тех, что решают задачу: выбирайте тип умного указателя так, чтобы он наиболее понятно объяснял ваши намерения и ваши нужды (на данный момент). Если вы создаете новый объект и не уверены, что потом вам понадобится реализовать совместное владение этим объектом, используйте unique_ptr, который реализует единоличное владение. Вы по-прежнему можете положить такой указатель в контейнер (например, vector<unique_ptr<widget> >) и работать с ним в большинстве случаев так же, как и с сырым указателем, только делая это безопасно. Если в дальнейшем вам понадобится совместное владение объектом, вы всегда можете преобразовать unique_ptr в shared_ptr с помощью перемещения (move-convert).
Во-вторых, unique_ptr более эффективен, чем shared_ptr. unique_ptr не нужно поддерживать счетчик ссылок и блок управления, и он разработан так, чтобы дополнительные расходы на его использование были минимальными по сравнению с сырым указателем. А когда вы не просите о большем, нежели вам нужно, вы не будете нести дополнительные расходы на что-то, что вы не используете.
В-третьих, использовать unique_ptr в качестве первой опции - это более гибкий подход, который позволяет перейти в дальнейшем к другим вариантам. Если изначально используется unique_ptr, его можно позже преобразовать в shared_ptr с помошью перемещения, или в любой другой умный указатель (или даже сырой указатель) - с помощью .get() или .release().
Рекомендация: Используйте стандартные умные указатели: unique_ptr по умолчанию и shared_ptr в случае, если необходимо совместное владение объектом. Это - стандартные типы, которые известны любой библиотеке на С++. Используйте умные указатели других типов только если необходимо обеспечить совместимость с другими библиотеками или если необходимо реализовать нестандартное поведение, которого нельзя достичь с помощью стандартных умных указателей.

2. Почему практически всегда следует использовать make_shared для создания объекта, которым будут впоследствии владеть shared_ptr? Объясните.

Примечание: если вам нужно создать объект с помощью нестандартного аллокатора (что случается довольно редко), используйте allocate_shared. Не смотря на то, что название allocate_shared немного отличается от make_shared, его следует читать как "просто разновидность make_shared, которая позволяет указывать аллокатор". Поэтому я в большинстве случаев буду иметь в виду обе эти функции, говоря здесь о make_shared, и не буду делать больших различий меджду ними.
Существует два основных случая, когда вы не можете использовать make_shared (или allocate_shared) для создания объекта, которым в дальнейшем будут владеть умные указатели shared_ptr.
(a) Если вам необходимо реализовать нестандартное поведение при удалении объекта (нестандартный делитер, custom deleter) - например, при использовании shared_ptr для управления не памятью, а каким-либо другим ресурсом, или для управления объекта, память под который выделена нестандартным способом. В этом случае вы не можете использовать функцию make_shared, потому что она не поддерживает возможность казать нестандартный делитер.
(b) Если вы получаете сырой указатель на объект, который передается вам из чужого кода (чаще всего унаследованного) - в этом случае вы создадите shared_ptr из этого указателся с помощью конструктора.
Рекомендация: Всегда используйте make_shared (или, если вам нужен нестандартный аллокатор, allocate_shared) чтобы создать объект, который будет управляться с помощью shared_ptr, кроме случаев, когда вам необходимо использовать нестандартный делитер (custom deleter), или когда вы получаете сырой указатель на объект извне.
Так почему же следует использовать make_shared (или, если вам нужен нестандартный аллокатор, allocate_shared) всегда, когда это возможно (то есть практически всегда)? На это есть две причины: простота и эффективность.
Во-первых, с make_shared код получается проще. Пишите в первую очередь понятно и корректно.
Во-вторых, код, использущий make_shared, получается эффективнее. Реализация shared_ptr должна обеспечивать хранение дополнительной информации в блоке управления, доступ к которой должен быть у всех объектов shared_ptr и weak_ptr, которые ссылаются на объект. На самом деле, это дополнительная информация должна включать в себя не один, а сразу два счетчика ссылок:

  • счетчик "сильных ссылок", чтобы отслеживать число объектов shared_ptr, которые не дают удалить объект. Объект разрушается (и возможно удаляется из памяти), когда последняя строгая ссылка исчезает;
  • счетчик "слабых ссылок", чтобы отслеживать число объектов weak_ptr, которые в данный момент указывают на объект. Разделяемый блок управления разрушается и удаляется из памяти (и разделяемый объект удаляется из памяти, если этого еще не случилось), когда исчезает последняя слабая ссылка.
Если вы отдельно создаете обхект с помощью оператора new и затем передаете полученный сырой указатель в конструктор shared_ptr, реализации shared_ptr ничего не остается, как отдельной операцией выделить память под блок управления, как показано в примере 2(a) и на рисунке 2(a).
// Пример 2(a): Отдельное выделение памяти под объект и под блок управления
auto sp1 = shared_ptr< widget >{ new widget{ } };
auto sp2 = sp1;
Рисунок 2(a): Возможное расположение объектов в памяти для примера 2(a)
Хорошо было бы избежать двух отдельных операций выделения памяти. Если вы используете функцию make_shared, чтобы создать и разделяемый объект и shared_ptr, реализация может сделать это за одну операцию выделения памяти, как показано в примере 2(b) и на рисунке 2(b).
// Пример 2(b): Одна операция выделения памяти и под объект и под блок управления
auto sp1 = make_shared< widget >( );
auto sp2 = sp1;
Рисунок 2(b): Возможное расположение объектов в памяти для примера 2(b)
Заметим, что совмещение двух операций выделения памяти в одну имеет два главных преимущества.
  • Уменьшаются дополнительные издержки на выделение памяти, в том числе и фрагментация памяти. Во-превых, наиболее очевидно издержки снижаются за счет снижения количества запросов на выделение памяти, которые обычно являются наиболее дорогими операциями. Это также снижает возможность конкуренции внутри аллокаторов (некоторые аллокаторы не очень хорошо масштабируются). Во-вторых, когда вы запрашиваете один блок памяти вместо двух, вы тем самым снижаете дополнительные расходы на саму операцию выделения памяти: при каждом запросе на блок памяти система должна выделить вам по крайней мере такое число байтов, которое вы запросили, но чаще она выделяет немного больше из-за использования пулов фиксированного размера или необходимости хранить дополнительную информацию о выделенном блоке. Поэтому используя один блок памяти вместо двух мы уменьшаем количество выделенной дополнительной памяти. В конце концов, мы также уменьшаем возможность появляения "мертвых" блоков памяти - небольших блоков, находящихся между уже выделенными, которые не будут выделены из-за своего размера, и которые приводят к фрагментации памяти.
  • Улучшается схема расположения объектов в памяти. Обычно счетчики ссылок используются в одном месте с объектом, на который ссылается указатель, и в случае маленьких объектов они скорее всего расположатся в одной кэш-линии, что улучшит производительность кэша (только в том случае, если какой-нибудь поток не начинает активно копировать умный указатель в цикле; не делайте так).
Как обычно, когда вы пытаетесь достичь большего за меньшее количество вызовов функций, вы помогаете системе выполнить всю работу более эффективно. Это одинаково справедливо и в случае, когда вы вставляете 100 элементов в vector с помощью одного вызова v.insert( first, last ), вместо ста вызовов v.insert( value ), и в случае, когда вы используете один вызов make_shared вместо отдельных вызовов new widget( ) и shared_ptr( widget* ).
Есть еще два преимущества: использование make_shared позволяет избежать явного вызова new и позволяет избежать проблем с безопасностью исключений. Оба этих соображения также относятся и к make_unique, поэтому поговорим о них в пункте №3.

3. Почему практически всегда следует использовать make_unique для создания объекта, которым будет владеть unique_ptr? Объясните.

Как и в случае с make_shared, есть два случая, когда вы не можете воспользоваться make_unique при создании объекта, которым будет управлять (хотя бы сначала) unique_ptr: либо вам нужен нестандартный делитер, либо вы принимаете сырой указатель на объект извне.
В других случаях, то есть практически всегда, предпочитайте make_unique.
Рекомендация: Всегда используйте make_unique чтобы создать объект, который не предполагает совместного владения (по крайней мере, на данный момент), кроме случаев, когда вам необходимо использовать нестандартный делитер (custom deleter), или когда вы получаете сырой указатель на объект извне.
Помимо того, что преимущества использования make_unique аналогичны рассмотренным ранее для make_shared, есть еще по крайней мере два соображения. Во-первых, используйте make_unique<T>() вместо unique_ptr<T>{ new T{ } }, потому что в принципе стоит избегать явных вызовов new.
Рекомендация: Не используйте явные вызовы new, delete и владеющие сырые указатели * кроме как в редких случаях, скрытых за реализациями низкоуровневых структор данных.
Во-вторых, использование make_unique позволяет избежать некоторых проблем с безопасностью исключений при использовании простого new. Рассмотрим пример.
void sink( unique_ptr< widget >, unique_ptr< gadget > );

sink( unique_ptr< widget >{ new widget{ } },
      unique_ptr< gadget >{ new gadget{ } } ); // Вопрос1: вы видите проблему?
Если вы сначала выделяете память под объект widget и создаете сам объект, а потом получаете исключение во время создания объекта gadget, объект widget не будет удален - утечка памяти. Вы можете возразить: "мы можем просто поменять new widget{ } на make_unique< widget >( ) и проблема будет решена, правильно?". То есть сделать так:
sink( make_unique< widget >( ),
      unique_ptr< gadget >{ new gadget{ } } ); // Вопрос2: так лучше?
Ответ - нет, так как стандарт C++ не определяет порядок вычисления аргументов функции, поэтому и make_unique< widget >( ) и new gadget( ) могут быть вызваны первыми. Если объект gadget создается первым, а make_unique< widget > выбрасывает исключение, мы имеем ту же самую проблему. При этом, хотя изменив один аргумент на make_unique мы не решаем проблему, изменив оба мы полностью от нее избавимся.
sink( make_unique< widget >( ), make_unique< gadget >( ) // Безопасно относительно исключений
Эта проблема с бeзопасностью исключений более полно описана в GotW #102: Exception-Safe Function Calls (мой перевод: С++: Безопасность относительно исключений при вызове функций).
Рекомендация: Для создания объекта используйте по умолчанию make_unique. Используйте make_shared если вы знаете, что объект будет управляться с помощью shared_ptr.

4. Что такое auto_ptr?

Существование auto_ptr может быть объяснено доблестной попыткой создать unique_ptr до того, как в С++ появилась семантика перемещения. auto_ptr признан устаревшим и не должен использоваться в новом коде.
Если у вас есть код, в котором используется auto_ptr, попробуйте сделать автоматический поиск и замену auto_ptr на unique_ptr. В подавляющем большинстве случаев все будет работать так же, как раньше, а в некоторых эта замена проявит (через ошибку компиляции) или незаметно исправит баг, о котором вы даже не знали.

1 комментарий :

  1. “Я надзвичайно задоволений послугою позики пана Лі, і ваші пропозиції допомогли мені отримати дуже вигідну угоду на житло. Я був би радий рекомендувати ваші послуги своїм друзям, які шукають позику ».
    Не шукайте нікуди більше, якщо ви шукаєте позику. Містер Лі найкращий. Я настійно рекомендую його послуги. Найкраще, що я коли-небудь мав задоволення працювати з його низькою ставкою позики. ”Це містер Лі Контакт, якщо хтось тут шукає позику. Whatsapp: + 1-989-394-3740. & Електронна адреса: 247officedept@gmail.com

    ОтветитьУдалить