Різниця в make_shared і нормальному shared_ptr в C ++


276
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 - з чого він починає у відео, яке я зв'язав вище, якщо це допомагає.
— chris

4
Перевага стилю малого коду: використовуючи make_sharedможливість запису, auto p1(std::make_shared<A>())p1 матиме правильний тип.
— Іван Вергілієв

Відповіді:


333

Різниця полягає в тому, що він std::make_sharedвиконує одне купірування, тоді як виклик std::shared_ptrконструктора виконує два.

Де відбуваються купи-виділення?

std::shared_ptr управляє двома суб'єктами:

  • блок управління (зберігає метадані, такі як перерахунок, стираний видалювач тощо)
  • керований об’єкт

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

Повинен бути спосіб weak_ptrs визначити, чи керований об'єкт все-таки дійсний (наприклад, для lock). Вони роблять це шляхом перевірки кількості shared_ptrs, що володіє керованим об'єктом, що зберігається в блоці управління. Результат полягає в тому, що блоки керування живі, доки shared_ptrпідрахунок та weak_ptrкількість не потрапляють у 0.

Повертатися до std::make_shared

Оскільки std::make_sharedздійснює єдиний розподіл купи як для керуючого блоку, так і для керованого об'єкта, немає можливості звільнити пам'ять для блоку управління та керованого об'єкта незалежно. Треба чекати, поки ми зможемо звільнити і блок управління, і керований об’єкт, що трапляється, поки немає живих shared_ptrабо weak_ptrs.

Припустимо, ми замість цього виконали два купі-виділення для керуючого блоку та керованого об'єкта через newта shared_ptrконструктор. Тоді ми звільняємо пам'ять керованого об'єкта (можливо, раніше), коли немає shared_ptrживих, і звільняємо пам'ять для блоку управління (можливо, пізніше), коли немає weak_ptrживих.


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){}
};

Я зіткнувся з цією точною проблемою і вирішив використати new, інакше я б використав make_shared. Ось відповідне питання щодо цього: stackoverflow.com/questions/8147027/… .
— jigglypuff

6

Якщо вам потрібне спеціальне вирівнювання пам’яті на об’єкті, керованому спільно_ptr, ви не можете покластися на make_shared, але я думаю, що це єдина вагома причина того, що його не використовувати.


2
Друга ситуація, коли make_shared є невідповідною, - це коли потрібно вказати спеціальний делетер.
— KnowItAllWannabe

5

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


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


4
Це випробування дещо безглуздо. Чи виявився тест у конфігурації випуску з оптимізаціями? Також всі ваші предмети звільняються негайно, тому це не реально.
— Phil1970

0

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.