Когда я хочу построить общий указатель из необработанного указателя

Благодаря std::make_shared мне интересно, имеет ли конструктор для std::shared_ptr, который принимает необработанный указатель, любое значение, кроме как при взаимодействии с устаревшим/библиотечным кодом, например. при сохранении вывода factory.

  • Существуют ли другие законные варианты использования?
  • Является ли разумным советом избегать этого конструктора?
  • Даже в дополнение к проверке кода на месте, предупреждающей программиста всякий раз, когда он используется?
  • Если к shared_ptr<T>::reset(T*) применяются те же рекомендации (независимо от того, что они есть)?

Что касается проверки кода: Я знаю, что взаимодействие с устаревшим/библиотечным кодом довольно распространено, поэтому автоматическая проверка кода может быть проблематичной, но в большинстве случаев я встречался до сих пор, я бы предпочел использовать unique_ptr в любом случае, и я также не говорю о компиляторе предупреждение, которое появляется в -Wall, а скорее правило для анализа статического кода во время проверки кода.


Моя мотивация:
Относительно легко сказать: "Не используйте std::shared_ptr<T>(new T(...)), всегда предпочитайте std::make_shared<T>(...)" (что я считаю правильным советом?). Но мне интересно, если это вообще не запах дизайна, если нужно создать shared_ptr из необработанного указателя, даже - или особенно - если объект был создан не только через new, потому что объект должен был быть создан как "общий" или "уникальный" объект в первую очередь.

Ответы

Ответ 1

Многие из ответов содержат хотя бы один уникальный аспект, поэтому я решил сделать сводный ответ. Кредит принадлежит @Niall, @Крис Дрю, @utnapistim, @isanae, @Markus Mayr и (по доверенности) Скотту Мейерсу.

Причины, по которым вы, возможно, не захотите/не сможете использовать make_shared,:

  • Вы должны использовать пользовательский деаэратор и/или даже пользовательский оператор распределения/новый.
    Это, вероятно, самая распространенная причина, особенно при взаимодействии с сторонними библиотеками. std::allocate_shared может помочь в некоторых из этих ситуаций.
  • Если память является проблемой, и у вас часто есть слабые указатели, которые переживают объект.
    Поскольку управляемый объект создается на том же блоке памяти, что и блок управления, память не может быть освобождена до тех пор, пока не будет уничтожен последний слабый указатель.
  • Если у вас есть частный конструктор и, например, только общедоступная функция factory.
    Создание make_shared друга не является жизнеспособным решением в этом случае, поскольку фактическая конструкция может произойти в некоторой вспомогательной функции/классе.

Что касается проверок кода, исключения, перечисленные выше, вероятно, слишком велики для автоматической проверки руководства "Использовать make_shared по возможности", а также для того, чтобы называть нарушение этого директивы дизайнерским запахом в своем собственном праве.

Как яркое пятно на горизонте, даже если библиотеке нужны пользовательские функции распределения и деллалокатора, и мы не можем/не хотим использовать std::allocate_shared, мы можем сделать свою собственную версию make shared, которая, по крайней мере, инкапсулирует вызов выделения и удаления и повышает безопасность исключений (хотя, скорее всего, он не предлагает единственного преимущества распределения).

Ответ 2

Первый случай использования, который появляется на ум, - это когда дебетер не является значением по умолчанию delete.

например. в среде Windows иногда должны использоваться COM-объекты, выпуск этих объектов должен выполняться на самом объекте через Release. Конечно, ATL можно использовать, но не все хотят его использовать.

struct ReleaseCom {
  template <class T>
  void operator() (T* p) const
  {
    p->Release();
  }
};
IComInterface* p = // co created or returned as a result
std::share_ptr<IComInterface> sp(p, ReleaseCom());

Более необычная ситуация, но все еще актуальная, - это когда объект (дескриптор или даже необработанная память) настраивается в DLL, ОС или библиотеке и имеет собственную связанную функцию очистки, которая должна быть вызвана (что может или не может вызовите delete по очереди). Если задействовано распределение памяти std::allocate_shared предлагает более расширенное управление распределителем, который будет использоваться без отображения необработанного указателя.

Мое личное чувство заключается в том, что с учетом std::make_shared и std::allocate_shared требование о создании shared_ptr из исходных указателей становится все меньше и меньше. Даже вышеперечисленные случаи могут быть включены в функции распределения и управления полезностью, удаляя из основного кода бизнес-логики.

Ответ 3

Скотт Майерс перечисляет несколько исключений в Эффективном современном С++

Первое уже упоминалось. Если вам нужно указать пользовательский дебетер, вы не можете использовать make_shared.

Во-вторых, если вы находитесь в системе с проблемами памяти, и вы выделяете очень большой объект, то с помощью make_shared выделяется один блок как для объекта, так и для блока управления, который включает в себя слабый счетчик ссылок. Если у вас есть weak_ptr, указывающий на объект, то память не может быть освобождена. С другой стороны, если вы не использовали make_shared, то, как только последний shared_ptr будет удален, память для очень большого объекта может быть освобождена.

Ответ 4

• Существуют ли другие законные варианты использования?

Да: существует ситуация, когда ресурс не отображается на новый /delete:

handle_type APIXCreateHandle(); // third party lib
void APIXDestroyHandle(handle_type h); // third party lib

В этом случае вы захотите напрямую использовать конструктор.

• Является ли разумным советом избегать этого конструктора?

Когда функции перекрываются с make_shared, да.

• Если к shared_ptr:: reset (T *) применяются те же рекомендации (что бы они ни были)?

Думаю, они не должны. Если вы действительно этого хотите, вы можете заменить вызов reset назначением с результатом вызова std:: make_shared. Вместо этого я предпочел бы видеть вызов reset, хотя это было бы более явным в намерении.

Что касается проверки кода: я знаю, что взаимодействие с устаревшим/библиотечным кодом довольно распространено

Рассмотрите возможность переноса библиотеки сторонних разработчиков в интерфейс, который гарантирует, что возвращаемые значения будут уникальными. Это обеспечит вам точку централизации и возможность для других оптимизаций удобства/безопасности.

Относительно легко сказать [...] (что я считаю правильным советом?). Но мне интересно, если это не запах дизайна вообще

Это не дизайнерский запах, чтобы использовать его; только для его использования, когда std:: make_shared/std:: make_unique будет работать так же хорошо.

Изменить: чтобы задать точку вопроса: вы, вероятно, не сможете добавить для нее правило статического анализа, если вы также не добавите к нему список конвенций/исключений (т.е. кроме пользовательских удалений и слоев адаптации API сторонних разработчиков, make_shared и make_unique всегда должны использоваться "). Такое правило, вероятно, будет пропущено для некоторых ваших файлов.

Если вы используете конструкторы напрямую, но может быть хорошей идеей иметь их в функции, предназначенной только для этого (подобно тому, как make_unique и make_shared делают):

namespace api_x
{
    std::shared_ptr<handle_type> make_handle(); // calls APIXCreateHandle internally
                                                // also calls the constructor
                                                // to std::shared_ptr
}

Ответ 5

Используйте std::make_shared, когда можете.

Есть две причины, по которым вам это не избежать:

  • Вы хотите использовать пользовательский удалённый объект для своего объекта. Это может иметь место, если вы взаимодействуете с кодом, который не использует RAII. Другие ответы содержат более подробную информацию.

  • У вас есть пользовательский новый оператор, который вы хотите вызвать. std::make_shared не может вызывать его для вас, потому что все его назначение - выделить память один раз (хотя это не требуется стандартом). См. http://ideone.com/HjmFl1.

Обоснование std::make_shared заключается не в том, что вы избегаете ключевое слово new, но избегаете выделения памяти. Общий указатель должен установить некоторую структуру данных управления. Поэтому при настройке shared_ptr без make_shared вам нужно выполнить два распределения: один для объекта, другой для структуры управления. make_shared (обычно) выделяет только один (больший) блок памяти и строит содержащийся объект и структуру управления на месте. Причиной make_shared является (изначально было), что в большинстве случаев она более эффективна, а не то, что синтаксис красивее. Это также является причиной того, что в С++ 11 не было std::make_unique. (Как заметил ChrisDrew, я предположил, что std::make_unique был добавлен только для синтаксиса симметрия/более симпатичный. Это может помочь записать безопасный код исключения более компактным способом, хотя, см. Herb Sutter GotW # 102 или этот вопрос.)

Ответ 6

В дополнение к другим ответам вы не можете использовать make_shared, если конструктор является приватным, например, из функции factory.

class C
{
public:
    static std::shared_ptr<C> create()
    {
        // fails
        // return std::make_shared<C>();

        return std::shared_ptr<C>(new C);
    }
private:
    C();
};