Разница в make_shared и normal shared_ptr в С++

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++ допускает произвольный порядок вычисления подвыражений, один из возможных порядков:

  1. new Lhs("foo"))
  2. new Rhs("bar"))
  3. std::shared_ptr<Lhs>
  4. 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 не может знать, что он был создан на месте, и его можно безопасно удалить. или я что-то упустил? Я не думаю, что более строгие правила при оценке параметров функций здесь как-то помогают...