Ответ 1
Разница в том, что std::make_shared
выполняет одно выделение кучи, тогда как вызов конструктора std::shared_ptr
выполняет два.
Где происходит распределение кучи?
std::shared_ptr
управляет двумя объектами:
- управляющий блок (хранит метаданные, такие как ref-count, стирание типа и т.д.)
- управляемый объект
std::make_shared
выполняет однократное распределение кучи для пространства, необходимого как для блока управления, так и для данных. В другом случае new Obj("foo")
вызывает выделение кучи для управляемых данных, а конструктор std::shared_ptr
выполняет другое для блока управления.
Для получения дополнительной информации ознакомьтесь с замечаниями по реализации на cppreference.
Обновление I: Исключительная безопасность
ПРИМЕЧАНИЕ (2019/08/30): это не проблема с C++ 17 из-за изменений в порядке оценки аргументов функции. В частности, каждый аргумент функции должен полностью выполняться перед вычислением других аргументов.
Поскольку ОП, кажется, задается вопросом об аспектах безопасности и исключений, я обновил свой ответ.
Рассмотрите этот пример,
void F(const std::shared_ptr<Lhs> &lhs, const std::shared_ptr<Rhs> &rhs) { /* ... */ }
F(std::shared_ptr<Lhs>(new Lhs("foo")),
std::shared_ptr<Rhs>(new Rhs("bar")));
Поскольку C++ допускает произвольный порядок вычисления подвыражений, один из возможных порядков:
new Lhs("foo"))
new Rhs("bar"))
std::shared_ptr<Lhs>
std::shared_ptr<Rhs>
Теперь предположим, что мы получаем исключение на шаге 2 (например, исключение нехватки памяти, конструктор Rhs
выдал некоторое исключение). Затем мы теряем память, выделенную на шаге 1, так как ничто не сможет очистить ее. Суть проблемы заключается в том, что необработанный указатель не был сразу передан конструктору std::shared_ptr
.
Один из способов исправить это - сделать их в отдельных строках, чтобы это произвольное упорядочение не происходило.
auto lhs = std::shared_ptr<Lhs>(new Lhs("foo"));
auto rhs = std::shared_ptr<Rhs>(new Rhs("bar"));
F(lhs, rhs);
Конечно, предпочтительным способом решения этой проблемы является использование std::make_shared
.
F(std::make_shared<Lhs>("foo"), std::make_shared<Rhs>("bar"));
Обновление II: недостаток std::make_shared
Цитируя Кейси комментарии:
Поскольку там есть только одно распределение, память-получатель не может быть освобождена, пока блок управления больше не используется.
weak_ptr
может поддерживать блок управления на неопределенный срок.
Почему экземпляры weak_ptr
поддерживают блок управления живым?
Должен быть способ weak_ptr
определить, является ли управляемый объект еще действительным (например, для lock
). Они делают это, проверяя количество shared_ptr
, которому принадлежит управляемый объект, который хранится в блоке управления. В результате блоки управления остаются активными до тех пор, пока счетчик shared_ptr
и weak_ptr
не достигнут 0.
Вернуться к std::make_shared
Поскольку std::make_shared
выполняет одно выделение кучи как для блока управления, так и для управляемого объекта, нет способа освободить память для блока управления и управляемого объекта независимо. Мы должны подождать, пока мы не сможем освободить как блок управления, так и управляемый объект, что происходит до тех пор, пока shared_ptr
или weak_ptr
не останется в живых.
Предположим, что вместо этого мы выполнили два выделения кучи для блока управления и управляемого объекта с помощью конструктора new
и shared_ptr
. Затем мы освобождаем память для управляемого объекта (возможно, раньше), когда живого shared_ptr
нет, и освобождаем память для блока управления (возможно, позже), когда живого weak_ptr
нет.