умные указатели — устройство и практикаsmart pointers — internals and practice

make_shared vsvs new

std::make_shared<T>(args...) почти всегда предпочтительнее std::shared_ptr<T>(new T(args...)) — но не из абстрактных соображений «стиля», а по конкретной, измеримой причине: он объединяет две отдельные аллокации (объект + control block) в одну, что меняет и производительность, и — в одном специфичном случае — паттерн использования памяти с weak_ptr.std::make_shared<T>(args...) is almost always preferable to std::shared_ptr<T>(new T(args...)) — not for abstract "style" reasons, but for one concrete, measurable one: it merges two separate allocations (the object + the control block) into one, which affects both performance and, in one specific case, the memory-usage pattern with weak_ptr.

одна аллокация вместо двухone allocation instead of two частый вопрос на собеседованииcommon interview question

new + shared_ptr: две независимые аллокацииnew + shared_ptr: two independent allocations

Написав std::shared_ptr<Widget> p(new Widget());, происходит две отдельные аллокации в куче: одна — под сам Widget (сделанная явным new), другая — под control block (сделанная конструктором shared_ptr уже после того, как объект создан). Это два отдельных обращения к аллокатору, потенциально в совершенно разных, далёких друг от друга участках памяти.Writing std::shared_ptr<Widget> p(new Widget()); triggers two separate heap allocations: one for the Widget itself (done by the explicit new), another for the control block (done by shared_ptr's constructor after the object already exists). That's two separate calls to the allocator, potentially landing in entirely different, distant regions of memory.

объект WidgetWidget object аллокация №1allocation #1 control block аллокация №2, где-то ещёallocation #2, somewhere else физически далеко друг от друга — разные обращения к аллокаторуphysically far apart — separate allocator calls

new Widget() + shared_ptr — два обращения к кучеtwo heap round-trips

make_shared: одна аллокация на оба блока сразуmake_shared: one allocation for both blocks at once

std::make_shared<Widget>(args...) выделяет один непрерывный кусок памяти, в котором control block и сам объект Widget размещены рядом (обычно control block непосредственно перед объектом или объединены в одну структуру). Конструктор Widget вызывается через placement new прямо внутри уже выделенной памяти.std::make_shared<Widget>(args...) allocates a single contiguous chunk of memory, in which the control block and the Widget object itself sit next to each other (typically the control block immediately before the object, or merged into one structure). Widget's constructor runs via placement new right inside that already-allocated memory.

control block Widget одна аллокацияone allocation

make_shared<Widget>(...) — один блок памяти, одно обращение к аллокаторуone memory block, one allocator call

Практический выигрыш двойной: во-первых, одно обращение к аллокатору вместо двух — сама аллокация памяти не бесплатна (в многопоточной программе она может требовать внутренней блокировки в аллокаторе). Во-вторых, кэш-локальность: control block и данные объекта физически рядом, поэтому чтение счётчика ссылок и чтение полей объекта чаще попадают в одну и ту же кэш-линию процессора, а не требуют двух отдельных промахов кэша в двух разных, потенциально далёких участках памяти.The practical win is twofold: first, one allocator call instead of two — the allocation itself isn't free (in a multi-threaded program it may require an internal lock inside the allocator). Second, cache locality: the control block and the object's data sit physically next to each other, so reading the reference count and reading the object's fields more often land in the same CPU cache line, rather than requiring two separate cache misses in two different, potentially distant memory regions.

// хуже: два обращения к аллокатору, два потенциальных промаха кэша// worse: two allocator calls, two potential cache misses
std::shared_ptr<Widget> a(new Widget(42));

// лучше: одна аллокация, control block и Widget рядом в памяти// better: one allocation, control block and Widget adjacent in memory
auto b = std::make_shared<Widget>(42);

Обратная сторона: make_shared и долгоживущие weak_ptrThe flip side: make_shared and long-lived weak_ptr

Поскольку control block и объект теперь занимают один и тот же блок памяти, вся эта память не может быть освобождена, пока не умрут оба — и объект (когда strong count дойдёт до нуля), и control block (когда weak count тоже дойдёт до нуля). При new+shared_ptr с раздельными аллокациями память самого объекта освобождается сразу при strong count == 0, а память control block-а (обычно намного меньшая) продолжает жить отдельно, пока жив хоть один weak_ptr.Because the control block and the object now share one single memory block, none of that memory can be freed until both die — the object (once the strong count hits zero) and the control block (once the weak count also hits zero). With separate new+shared_ptr allocations, the object's memory is freed immediately at strong count == 0, while the (usually much smaller) control block's memory keeps living separately for as long as any weak_ptr survives.

Практическое следствие: если объект большой (скажем, содержит буфер на несколько мегабайт), а какой-то долгоживущий weak_ptr на него держится намного дольше, чем сам объект, — с make_shared вся эта память объекта не будет освобождена всё это время, хотя сам объект давно логически уничтожен. Это редкий, но реальный случай, когда явный new с раздельной аллокацией предпочтительнее make_shared.The practical consequence: if the object is large (say, it holds a multi-megabyte buffer) and some long-lived weak_ptr to it outlives the object by a lot, then with make_shared all of that object's memory stays unreleased the whole time, even though the object itself was logically destroyed long ago. This is a rare but real case where an explicit new with a separate allocation beats make_shared.

Дополнительные причины предпочитать make_sharedAdditional reasons to prefer make_shared

Исключительная безопасность в списке аргументов вызоваException safety in a call's argument list

До гарантированного порядка вычисления аргументов (C++17) выражение вида f(std::shared_ptr<A>(new A()), std::shared_ptr<B>(new B())) могло теоретически выполнить оба new, а потом уже конструировать оба shared_ptr — если между ними бросалось исключение, один из сырых указателей мог утечь, ни разу не будучи обёрнутым в shared_ptr. make_shared не создаёт такого промежуточного «голого» указателя, который потенциально доступен снаружи.Before evaluation order for arguments was guaranteed (C++17), an expression like f(std::shared_ptr<A>(new A()), std::shared_ptr<B>(new B())) could theoretically run both news first and only then construct both shared_ptrs — if an exception was thrown in between, one of the raw pointers could leak without ever being wrapped. make_shared never creates such an intermediate "naked" pointer exposed to the outside.

Меньше повторения имени типаLess repetition of the type name

make_shared<Widget>(args...) называет тип один раз; shared_ptr<Widget>(new Widget(args...)) называет его дважды — небольшая, но реальная возможность рассинхронизации (например, при рефакторинге поменять тип в одном месте и забыть в другом).make_shared<Widget>(args...) names the type once; shared_ptr<Widget>(new Widget(args...)) names it twice — a small but real opportunity for the two to drift out of sync (say, during a refactor, changing the type in one spot and forgetting the other).

Когда make_shared нельзя использоватьWhen make_shared can't be used

make_shared не подходит, если нужен кастомный deleter (у него нет параметра для этого — только у самого конструктора shared_ptr есть перегрузка, принимающая deleter), или если конструктор класса приватный/защищённый, а доступ разрешён только через friend-объявленную фабричную функцию, которая не может напрямую вызвать make_shared изнутри (обходится через приватный тег-конструктор или std::allocate_shared).make_shared doesn't work if a custom deleter is needed (it has no parameter for one — only shared_ptr's own constructor has an overload taking a deleter), or if the class's constructor is private/protected and access is only granted through a friend-declared factory function that can't call make_shared directly from inside (worked around via a private tag constructor or std::allocate_shared).