Різниця в 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.