умные указателиsmart pointers
Совместное владение объектом через атомарный подсчёт ссылок: объект удаляется, когда последний shared_ptr на него уничтожается. Гибкость стоит дороже unique_ptr — atomic inc/dec на каждое копирование.Shared ownership of an object through atomic reference counting: the object is destroyed when the last shared_ptr pointing to it is destroyed. That flexibility costs more than unique_ptr — an atomic inc/dec on every copy.
std::shared_ptr<T> — умный указатель с разделяемым владением: несколько shared_ptr могут одновременно владеть одним объектом через общий control block со счётчиком ссылок. Объект уничтожается, когда счётчик доходит до нуля — то есть когда уничтожен последний владеющий shared_ptr.std::shared_ptr<T> is a smart pointer with shared ownership: multiple shared_ptr instances can simultaneously own the same object through a common control block holding a reference count. The object is destroyed when the count reaches zero — that is, when the last owning shared_ptr is destroyed.
std::unique_ptr<Widget> a = std::make_unique<Widget>();
// std::unique_ptr<Widget> b = a; // не компилируется// std::unique_ptr<Widget> b = a; // doesn't compile
std::shared_ptr<Widget> a = std::make_shared<Widget>();
std::shared_ptr<Widget> b = a; // ок, счётчик = 2// ok, count = 2
// объект удалится, когда оба a и b выйдут из скоупа// the object is destroyed once both a and b go out of scope
Некоторые задачи структурно не укладываются в единоличное владение unique_ptr:Some problems structurally don't fit unique_ptr's sole-ownership model:
Когда несколько независимых частей программы должны держать объект живым, пока хотя бы одна из них его использует (кэши, графы объектов, callback-и, сохранённые в разных местах) — единственный владелец не может это выразить, нужен подсчёт ссылок.When several independent parts of a program need to keep an object alive as long as at least one of them is using it (caches, object graphs, callbacks stashed in different places) — a single owner can't express that, reference counting is required.
До C++11 разделяемое владение реализовывалось вручную (intrusive refcounting) или через boost::shared_ptr (который во многом стал прообразом std::shared_ptr при стандартизации) — стандартная библиотека унифицировала это в один тип с чёткой, потокобезопасной семантикой счётчика.Before C++11, shared ownership was implemented by hand (intrusive refcounting) or via boost::shared_ptr (which largely became the template for std::shared_ptr during standardization) — the standard library unified this into a single type with clear, thread-safe counter semantics.
Копирование и уничтожение shared_ptr из разных потоков корректно инкрементирует/декрементирует общий счётчик благодаря atomic-операциям внутри — программисту не нужно оборачивать это в mutex самостоятельно (хотя доступ к самому объекту T всё равно требует своей синхронизации).Copying and destroying a shared_ptr from different threads correctly increments/decrements the shared counter thanks to atomic operations under the hood — the programmer doesn't need to wrap this in a mutex themselves (though access to the object T itself still requires its own synchronization).
shared_ptr хранит два указателя: на сам объект и на control block, который содержит два счётчика — strong count (число shared_ptr) и weak count (число weak_ptr, см. weak-ptr.html), а также deleter и allocator. Инкремент/декремент strong count — атомарная операция (обычно fetch_add/fetch_sub с memory_order_relaxed для inc и memory_order_acq_rel для финального dec) — это и есть источник overhead относительно unique_ptr.shared_ptr stores two pointers: one to the object itself, and one to the control block, which holds two counters — the strong count (number of shared_ptr instances) and the weak count (number of weak_ptr instances, see weak-ptr.html), plus a deleter and an allocator. Incrementing/decrementing the strong count is an atomic operation (typically fetch_add/fetch_sub with memory_order_relaxed for increment and memory_order_acq_rel for the final decrement) — this is the source of overhead relative to unique_ptr.
auto p1 = std::make_shared<Widget>(); // одна аллокация: объект + control block вместе// one allocation: object + control block together
auto p2 = p1; // atomic increment strong count → 2// atomic increment of strong count → 2
p1.reset(); // atomic decrement → 1, p1 = nullptr// atomic decrement → 1, p1 = nullptr
// p2 всё ещё владеет объектом, объект жив// p2 still owns the object, the object is alive
// когда p2 тоже уничтожится — decrement → 0 → delete объекта// once p2 is also destroyed — decrement → 0 → the object is deleted
std::cout << p2.use_count(); // текущее значение strong count (только для диагностики,// the current strong count value (for diagnostics only,
// не для логики — race между чтением и concurrent copy/reset)// not for logic — a race exists between the read and a concurrent copy/reset)
// std::shared_ptr<Widget>(new Widget()) — две отдельные аллокации// std::shared_ptr<Widget>(new Widget()) — two separate allocations
// (объект, потом control block) — make_shared делает одну, поэтому предпочтительнее// (the object, then the control block) — make_shared does one, so it's preferred
Сам объект T не защищён mutex-ом через shared_ptr — потокобезопасен только счётчик ссылок. Конкурентное чтение/запись в сам T требует собственной синхронизации, как и для любого разделяемого состояния.The object T itself isn't protected by a mutex through shared_ptr — only the reference count is thread-safe. Concurrent reads/writes to T itself require their own synchronization, same as any other shared state.
Ожидаемый на интервью объём: control block с atomic-счётчиком, copy инкрементит, деструктор декрементит и удаляет объект при достижении нуля. Без weak_ptr и без единой аллокации make_shared — это уже отдельные усложнения, которые обычно не спрашивают сходу.The scope expected in an interview: a control block with an atomic counter, copy increments it, the destructor decrements it and deletes the object once it reaches zero. Without weak_ptr and without make_shared's single-allocation trick — those are separate complications that usually aren't asked upfront.
my_shared_ptr.hpp
#include <atomic>
template <class T>
class my_shared_ptr {
T* ptr_ = nullptr;
std::atomic<long>* count_ = nullptr;
void release() {
if (count_ && count_->fetch_sub(1, std::memory_order_acq_rel) == 1) {
delete ptr_;
delete count_;
}
}
public:
my_shared_ptr() noexcept = default;
explicit my_shared_ptr(T* p)
: ptr_(p), count_(p ? new std::atomic<long>(1) : nullptr) {}
// copy — инкремент счётчика// copy — increments the counter
my_shared_ptr(const my_shared_ptr& other) noexcept
: ptr_(other.ptr_), count_(other.count_) {
if (count_) count_->fetch_add(1, std::memory_order_relaxed);
}
my_shared_ptr& operator=(const my_shared_ptr& other) noexcept {
if (this != &other) {
release();
ptr_ = other.ptr_;
count_ = other.count_;
if (count_) count_->fetch_add(1, std::memory_order_relaxed);
}
return *this;
}
// move — без атомарной операции, просто забираем указатели// move — no atomic operation, we simply take over the pointers
my_shared_ptr(my_shared_ptr&& other) noexcept
: ptr_(other.ptr_), count_(other.count_) {
other.ptr_ = nullptr;
other.count_ = nullptr;
}
~my_shared_ptr() { release(); }
T& operator*() const { return *ptr_; }
T* operator->() const { return ptr_; }
T* get() const { return ptr_; }
long use_count() const {
return count_ ? count_->load(std::memory_order_relaxed) : 0;
}
};
Типичные уточняющие вопросы после базовой реализации: почему инкремент можно делать с memory_order_relaxed (копирующий поток уже владеет валидным shared_ptr — никакой другой поток не может параллельно довести счётчик до нуля и удалить объект, пока жива хотя бы одна копия), а финальный декремент — с memory_order_acq_rel (нужен acquire, чтобы гарантированно увидеть все обращения к объекту из других потоков-«совладельцев» перед его удалением — тот же паттерн, что в atomic.html).Typical follow-up questions after the basic implementation: why the increment can use memory_order_relaxed (the copying thread already owns a valid shared_ptr — no other thread can concurrently drive the counter to zero and delete the object while at least one copy is alive), and why the final decrement needs memory_order_acq_rel (an acquire is needed to guarantee visibility of all accesses to the object made by other "co-owner" threads before deleting it — the same pattern as in atomic.html).
Два объекта, держащие shared_ptr друг на друга (A владеет B, B владеет A), никогда не дойдут до нулевого счётчика — классическая утечка, характерная именно для reference counting. Разрывается через weak_ptr на одной из сторон цикла (подробнее — weak-ptr.html).Two objects holding shared_ptr to each other (A owns B, B owns A) will never reach a zero count — a classic leak specific to reference counting. It's broken by using a weak_ptr on one side of the cycle (details in weak-ptr.html).
make_shared размещает объект и control block в одном блоке памяти — быстрее, но если есть хотя бы один weak_ptr, вся эта единая аллокация не освобождается, пока не умрёт последний weak_ptr, даже если сам объект T уже разрушен (только вызван деструктор, память ещё занята под control block).make_shared places the object and control block in a single memory block — faster, but if even one weak_ptr exists, that entire combined allocation isn't freed until the last weak_ptr dies, even if the object T itself has already been destroyed (only its destructor has run — the memory is still occupied by the control block).
Создание нового независимого shared_ptr<T>(this) внутри метода класса заводит второй, несвязанный control block — оба control block решат, что владеют объектом единолично, и объект будет удалён дважды. Правильный способ — наследование от std::enable_shared_from_this<T> и вызов shared_from_this().Creating a new, independent shared_ptr<T>(this) inside a class method sets up a second, unrelated control block — both control blocks will think they solely own the object, and it gets deleted twice. The correct way is to inherit from std::enable_shared_from_this<T> and call shared_from_this().