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

std::weak_ptr

API уже разобран в stl/cpp11/weak-ptr.html — здесь про то, какую именно проблему weak_ptr решает (циклические ссылки между shared_ptr, приводящие к утечке), как это устроено через control block, и почему std::atomic<std::weak_ptr>, ожидавшийся в C++26, так и не был стандартизирован.The API is already covered in stl/cpp11/weak-ptr.html — this page covers exactly which problem weak_ptr solves (cyclic references between shared_ptrs causing a leak), how that's implemented through the control block, and why std::atomic<std::weak_ptr>, expected for C++26, never got standardized.

решает утечку от цикловfixes the cycle leak C++26: atomic<weak_ptr> — не вошёл— didn't make it

Проблема циклических ссылокThe cyclic-reference problem

Если A хранит shared_ptr<B>, а B хранит shared_ptr<A> в ответ, ни один из strong count-ов никогда не дойдёт до нуля, даже если вне этой пары объектов не осталось ни одной ссылки на них: A удерживает B живым, B удерживает A живым, взаимно, навсегда. Это утечка памяти — детерминированная и предсказуемая, но неустранимая обычным подсчётом ссылок.If A holds a shared_ptr<B> and B holds a shared_ptr<A> in return, neither strong count ever reaches zero, even if no reference to either object remains anywhere outside this pair: A keeps B alive, B keeps A alive, mutually, forever. This is a memory leak — deterministic and predictable, but unfixable by plain reference counting alone.

struct B;
struct A { std::shared_ptr<B> b; ~A() { std::cout << "~A\n"; } };
struct B { std::shared_ptr<A> a; ~B() { std::cout << "~B\n"; } };

{
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();
    a->b = b;  // strong(B) = 2// strong(B) = 2
    b->a = a;  // strong(A) = 2// strong(A) = 2
}  // a, b выходят из скоупа: strong(A)=1, strong(B)=1 — ни один не дошёл до 0// a, b go out of scope: strong(A)=1, strong(B)=1 — neither reaches 0
   // ни "~A", ни "~B" никогда не напечатаются — оба объекта утекли навсегда// neither "~A" nor "~B" ever prints — both objects leak forever

Как weak_ptr разрывает циклHow weak_ptr breaks the cycle

weak_ptr хранит те же два указателя, что и shared_ptr (на объект и на control block), но не увеличивает strong count — только weak count. Разорвать цикл нужно на одном из направлений отношения: если связь концептуально «дочерняя ссылка на родителя» (child знает про parent, но не наоборот по владению), эта сторона должна быть weak_ptr, а не shared_ptr.weak_ptr stores the same two pointers shared_ptr does (to the object and to the control block), but doesn't increment the strong count — only the weak count. The cycle needs to be broken on one direction of the relationship: if the link is conceptually "a child pointing back at its parent" (the child knows about the parent, but not the other way around in terms of ownership), that side should be a weak_ptr, not a shared_ptr.

struct B;
struct A { std::shared_ptr<B> b; ~A() { std::cout << "~A\n"; } };
struct B { std::weak_ptr<A> a; ~B() { std::cout << "~B\n"; } };  // назад — weak, не shared// back-reference — weak, not shared

{
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();
    a->b = b;  // strong(B) = 2// strong(B) = 2
    b->a = a;  // strong(A) остаётся 1 — weak_ptr его не увеличивает// strong(A) stays at 1 — a weak_ptr doesn't bump it
}  // a выходит из скоупа: strong(A) -> 0, A разрушается, разрушает b внутри себя,// a goes out of scope: strong(A) -> 0, A is destroyed, destroying its own b,
   // strong(B) -> 0, B тоже разрушается. Печатает "~A" и "~B" — утечки нет// strong(B) -> 0, B is also destroyed. Prints "~A" and "~B" — no leak

Использовать объект через weak_ptr напрямую нельзя — сначала нужно вызвать .lock(), который атомарно проверяет strong count и либо возвращает полноценный shared_ptr (если объект ещё жив), либо пустой shared_ptr (если объект уже удалён). Это принудительно заставляет каждый доступ проверять «а жив ли ещё объект» — без этой проверки было бы легко случайно разыменовать уже удалённый указатель.An object can't be used through a weak_ptr directly — .lock() must be called first, which atomically checks the strong count and either returns a full shared_ptr (if the object is still alive) or an empty one (if it's already gone). This forces every access to check "is the object still alive" — without that check it would be easy to accidentally dereference an already-deleted pointer.

Control block переживает объектThe control block outlives the object

Как только strong count доходит до нуля, объект уничтожается — но control block не освобождается, пока не дойдёт до нуля и weak count. Это позволяет lock() безопасно проверять состояние даже после того, как объект физически исчез: control block всё ещё существует, чтобы честно ответить «объект уже мёртв».The moment the strong count reaches zero, the object is destroyed — but the control block isn't freed until the weak count also reaches zero. This lets lock() safely check the state even after the object has physically vanished: the control block still exists to honestly answer "the object is already dead."

Практическое следствие: держать долгоживущие weak_ptr на объекты, которые часто удаляются, — небольшая, но реальная утраченная память (сам control block, обычно несколько десятков байт), пока не будет уничтожен и последний weak_ptr.The practical consequence: holding long-lived weak_ptrs to objects that get destroyed often is a small but real memory cost (the control block itself, typically a few dozen bytes) until the last weak_ptr is also destroyed.

std::atomic<std::weak_ptr> — так и не вошёл в C++26— still didn't make it into C++26

C++20 добавил специализации std::atomic<std::shared_ptr<T>> и std::atomic<std::weak_ptr<T>>, позволяющие атомарно читать/писать сам умный указатель (а не только счётчик внутри него) без ручной блокировки мьютексом — раньше конкурентный доступ к общей переменной типа shared_ptr требовал отдельной синхронизации сверху, поскольку операции присваивания самого указателя (не счётчика) атомарными не были.C++20 added specializations for std::atomic<std::shared_ptr<T>> and std::atomic<std::weak_ptr<T>>, allowing atomic reads/writes of the smart pointer itself (not just the counter inside it) without a manual mutex — before that, concurrent access to a shared variable of type shared_ptr needed its own separate synchronization, since assigning the pointer itself (not the counter) wasn't atomic.

Обе специализации существуют с C++20, но на практике страдали от неполной и медленной реализации в основных стандартных библиотеках (нередко реализованы через внутренний мьютекс, а не через настоящие lock-free атомарные операции) — предложения по доработке и ускорению этого механизма обсуждались для C++26, но конкретно углублённые изменения/расширения вокруг atomic<weak_ptr> не были приняты в финальный стандарт C++26. Практический вывод: перед использованием стоит замерить производительность на конкретном компиляторе/библиотеке — «атомарный» не всегда означает «лёгкий».Both specializations have existed since C++20, but in practice suffered from incomplete and slow implementations in the major standard libraries (often implemented via an internal mutex rather than genuinely lock-free atomic operations) — proposals to refine and speed up this mechanism were discussed for C++26, but the specific deepened changes/extensions around atomic<weak_ptr> didn't make it into the final C++26 standard. The practical takeaway: benchmark on your specific compiler/library before relying on it — "atomic" doesn't always mean "cheap."

ГраблиGotchas

weak_ptr не решает утечки автоматически — решение архитектурное, не языковоеdoesn't fix leaks automatically — the fix is architectural, not linguistic

Компилятор не подскажет, какая из двух сторон отношения должна стать weak_ptr — это решение о том, кто кому концептуально принадлежит (родитель владеет ребёнком, не наоборот), должен принять разработчик. Механически заменить оба shared_ptr на weak_ptr тоже неверно — тогда никто вообще не будет владеть объектами.The compiler won't tell you which side of the relationship should become a weak_ptr — that's a decision about who conceptually owns whom (the parent owns the child, not the reverse), and it's the developer's call. Mechanically replacing both shared_ptrs with weak_ptr is also wrong — then nothing owns the objects at all.

lock() возвращает временный shared_ptr — держать его, а не вызывать повторноreturns a temporary shared_ptr — hold onto it, don't call it repeatedly

Проверка if (wp.lock()) с последующим отдельным wp.lock()->doSomething() — гонка: между двумя вызовами lock() объект теоретически может быть удалён другим потоком. Правильный паттерн — сохранить результат lock() в локальную переменную shared_ptr и использовать её, продлевая жизнь объекта на время работы с ним.Checking with if (wp.lock()) followed by a separate wp.lock()->doSomething() is a race — between the two lock() calls, another thread could theoretically delete the object. The correct pattern is to save lock()'s result into a local shared_ptr variable and use that, extending the object's life for as long as it's being worked with.