Когда я хочу построить общий указатель из необработанного указателя
Благодаря 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();
};