std::shared_ptr<Object> p1 = std::make_shared<Object>("foo");
std::shared_ptr<Object> p2(new Object("foo"));
В нем есть много сообщений google и stackoverflow, но я не могу понять, почему make_shared
более эффективен, чем напрямую с помощью shared_ptr
.
Может кто-нибудь объяснить мне поэтапную последовательность созданных объектов и операции, выполненные обоими, чтобы я мог понять, насколько эффективен make_shared
. Я привел один пример выше для справки.
Ответ 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
нет.
Ответ 2
Общий указатель управляет как самим объектом, так и небольшим объектом, содержащим счетчик ссылок и другие служебные данные. make_shared
может выделить один блок памяти для хранения обоих из них; для создания общего указателя из указателя на уже выделенный объект потребуется выделить второй блок для хранения счетчика ссылок.
Как и эта эффективность, использование make_shared
означает, что вам не нужно иметь дело с new
и необработанными указателями вообще, что дает лучшую безопасность исключений - нет возможности выбросить исключение после выделения объекта, но перед назначением его интеллектуальному указателю.
Ответ 3
Существует еще один случай, когда две возможности различаются, помимо тех, которые уже упомянуты: если вам нужно вызвать непубличный конструктор (защищенный или закрытый), make_shared может не иметь к нему доступа, а вариант с новые работы прекрасны.
class A
{
public:
A(): val(0){}
std::shared_ptr<A> createNext(){ return std::make_shared<A>(val+1); }
// Invalid because make_shared needs to call A(int) **internally**
std::shared_ptr<A> createNext(){ return std::shared_ptr<A>(new A(val+1)); }
// Works fine because A(int) is called explicitly
private:
int val;
A(int v): val(v){}
};
Ответ 4
Если вам требуется специальное выравнивание памяти для объекта, управляемого shared_ptr, вы не можете полагаться на make_shared, но я думаю, что это единственная хорошая причина не использовать его.
Ответ 5
Shared_ptr
: выполняет два распределения кучи
- Блок управления (счетчик ссылок)
- Управляемый объект
Make_shared
: Выполняет только одно распределение кучи
- Управляющий блок и данные объекта.
Ответ 6
Я вижу одну проблему с std::make_shared, он не поддерживает частные/защищенные конструкторы
Ответ 7
Что касается эффективности и времени, затраченного на выделение, я сделал этот простой тест ниже, я создал множество экземпляров с помощью этих двух способов (по одному):
for (int k = 0 ; k < 30000000; ++k)
{
// took more time than using new
std::shared_ptr<int> foo = std::make_shared<int> (10);
// was faster than using make_shared
std::shared_ptr<int> foo2 = std::shared_ptr<int>(new int(10));
}
Дело в том, что использование make_shared заняло двойное время по сравнению с использованием нового. Таким образом, используя новый, есть два распределения кучи вместо одного, использующего make_shared. Может быть, это глупый тест, но разве это не показывает, что использование make_shared занимает больше времени, чем использование нового? Конечно, я говорю только о времени.
Ответ 8
Я думаю, что вопрос безопасности исключений в ответе г-на mpark по-прежнему актуален. при создании shared_ptr вот так: shared_ptr & lt; T> (новый T), новый T может преуспеть, в то время как распределение shared_ptr блока управления может завершиться неудачей. в этом сценарии только что выделенный T будет просачиваться, так как shared_ptr не может знать, что он был создан на месте, и его можно безопасно удалить. или я что-то упустил? Я не думаю, что более строгие правила при оценке параметров функций здесь как-то помогают...