Разница в make_shared и обычном shared_ptr в C ++

277
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это эффективно. Я привел один пример выше для справки.

Ануп Бучке
источник
4
Это не более эффективно. Причиной его использования является исключительная безопасность.
Юиши
В некоторых статьях говорится, что это позволяет избежать некоторых накладных расходов на строительство, не могли бы вы объяснить подробнее?
Ануп Бучке
16
@Yuushi: Исключительная безопасность - хорошая причина, чтобы использовать это, но это также более эффективно.
Майк Сеймур
3
32:15 - это то, с чего он начинает видео, на которое я ссылался выше, если это поможет.
Крис
4
Незначительное преимущество стиля кода: используя make_sharedвы можете написать, auto p1(std::make_shared<A>())и p1 будет иметь правильный тип.
Иван Вергилиев

Ответы:

333

Разница в том, что 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, из-за изменений в порядке оценки аргументов функции. В частности, каждый аргумент функции должен полностью выполняться перед вычислением других аргументов.

Поскольку OP, кажется, задается вопросом о стороне безопасности исключений, я обновил свой ответ.

Рассмотрим этот пример,

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_ptrs поддерживают блок управления живым?

У weak_ptrs должен быть способ определить, является ли управляемый объект еще действительным (например, для lock). Они делают это, проверяя количество shared_ptrs, которым принадлежит управляемый объект, который хранится в блоке управления. В результате блоки управления остаются активными до тех пор, пока shared_ptrсчетчик и weak_ptrсчетчик не достигнут значения 0.

Вернуться к std::make_shared

Поскольку std::make_sharedвыполняется одно выделение кучи как для блока управления, так и для управляемого объекта, невозможно освободить память для блока управления и управляемого объекта независимо. Мы должны подождать, пока мы не сможем освободить как блок управления, так и управляемый объект, что происходит до тех пор, пока не останется shared_ptrs или weak_ptrs живыми.

Предположим, что вместо этого мы выполнили два выделения кучи для блока управления и управляемого объекта через newи shared_ptrконструктор. Затем мы освобождаем память для управляемого объекта (возможно, раньше), когда нет shared_ptrживых s, и освобождаем память для блока управления (возможно, позже), когда нет weak_ptrживых s.

mpark
источник
53
Также неплохо бы упомянуть небольшую make_sharedобратную сторону углового случая : поскольку имеется только одно распределение, память пуантиста не может быть освобождена, пока контрольный блок больше не используется. А weak_ptrможет держать блок управления живым бесконечно.
Кейси
14
Еще один, более стилистический момент: если вы используете make_sharedи make_uniqueпоследовательно, у вас не будет необработанных указателей, и вы можете рассматривать каждое вхождение newкак запах кода.
Филипп
6
Если есть только один shared_ptr, и нет weak_ptrs, вызов reset()на shared_ptrэкземпляр будет удален блок управления. Но это независимо от того, make_sharedбыл ли использован. Использование make_sharedимеет значение, потому что это может продлить срок службы памяти, выделенной для управляемого объекта . Когда shared_ptrколичество достигает 0, деструктор для управляемого объекта вызывается независимо от того make_shared, но освобождение его памяти может быть выполнено только в том случае, если make_sharedон не использовался. Надеюсь, что это делает это более ясным.
mpark
4
Стоит также упомянуть, что make_shared может воспользоваться оптимизацией «Мы знаем, где вы живете», которая позволяет блоку управления иметь меньший указатель. (Подробнее см. Презентацию Стефана Т. Лававея в GN2012 на минуте 12.) make_shared, таким образом, не только избегает выделения, но также выделяет меньше общего объема памяти.
KnowItAllWannabe
1
@HannaKhalil: Возможно, это то, что вы ищете ...? melpon.org/wandbox/permlink/b5EpsiSxDeEz8lGH
mpark
26

Общий указатель управляет как самим объектом, так и небольшим объектом, содержащим счетчик ссылок и другие служебные данные. make_sharedможет выделить один блок памяти для хранения обоих из них; Для создания общего указателя из указателя на уже выделенный объект потребуется выделить второй блок для хранения счетчика ссылок.

Помимо этой эффективности, использование make_sharedозначает, что вам вообще не нужно иметь дело с newнеобработанными указателями, что обеспечивает лучшую безопасность исключений - нет возможности генерировать исключение после выделения объекта, но до назначения его интеллектуальному указателю.

Майк Сеймур
источник
2
Я правильно понял ваш первый пункт. Можете ли вы уточнить или дать несколько ссылок на второй пункт о безопасности исключений?
Ануп Бучке
22

Есть еще один случай, когда две возможности отличаются друг от друга, помимо уже упомянутых: если вам нужно вызвать непубличный конструктор (защищенный или закрытый), 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){}
};
Dr_Sam
источник
Я столкнулся с этой точной проблемой и решил использовать new, иначе я бы использовал make_shared. Вот связанный вопрос об этом: stackoverflow.com/questions/8147027/… .
jigglypuff
6

Если вам нужно специальное выравнивание памяти на объекте, контролируемом shared_ptr, вы не можете полагаться на make_shared, но я думаю, что это единственная веская причина не использовать его.

Саймон Феркель
источник
2
Вторая ситуация, в которой make_shared не подходит, это когда вы хотите указать пользовательский удалитель.
KnowItAllWannabe
5

Я вижу одну проблему с std :: make_shared, он не поддерживает частные / защищенные конструкторы

icebeat
источник
3

Shared_ptr: Выполняет два выделения кучи

  1. Блок управления (счетчик ссылок)
  2. Управляемый объект

Make_shared: Выполняет только одно выделение кучи

  1. Блок управления и данные объекта.
Джеймс
источник
0

Что касается эффективности и времени, затрачиваемого на распределение, я сделал этот простой тест ниже, я создал много экземпляров с помощью этих двух способов (по одному за раз):

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 заняло в два раза больше времени, чем использование new. Таким образом, при использовании new существует два выделения кучи вместо одного с использованием make_shared. Может быть, это глупый тест, но разве он не показывает, что использование make_shared занимает больше времени, чем использование new? Конечно, я говорю только об использованном времени.

орландо
источник
4
Этот тест несколько бессмысленный. Был ли проведен тест в конфигурации релиза с полученными оптимизациями? Также все ваши предметы освобождаются сразу, поэтому это нереально.
Phil1970
0

Я думаю, что вопрос безопасности исключений в ответе г-на Мпарка по-прежнему актуален. при создании shared_ptr, например: shared_ptr <T> (новый T), новый T может быть успешным, в то время как выделение блока управления shared_ptr может завершиться неудачей. в этом сценарии только что выделенный T будет течь, так как shared_ptr не может знать, что он был создан на месте, и его можно безопасно удалить. или я что-то упустил? Я не думаю, что более строгие правила оценки параметров функций здесь могут помочь

Мартин Форбродт
источник