умные указателиsmart pointers

std::shared_ptr

Совместное владение объектом через атомарный подсчёт ссылок: объект удаляется, когда последний 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.

atomic refcount control block — отдельная аллокацияcontrol block — a separate allocation частый вопрос: реализуй сам (упрощённо)common interview question: implement it yourself (simplified)

Что этоWhat it is

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.

unique_ptr — один владелецunique_ptr — a single owner
std::unique_ptr<Widget> a = std::make_unique<Widget>();
// std::unique_ptr<Widget> b = a; // не компилируется// std::unique_ptr<Widget> b = a; // doesn't compile
shared_ptr — совместное владениеshared_ptr — shared ownership
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

Зачем ввелиWhy it was introduced

Некоторые задачи структурно не укладываются в единоличное владение unique_ptr:Some problems structurally don't fit unique_ptr's sole-ownership model:

01

Время жизни объекта не привязано к одному скоупуObject lifetime isn't tied to a single scope

Когда несколько независимых частей программы должны держать объект живым, пока хотя бы одна из них его использует (кэши, графы объектов, 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.

02

Замена бесконтрольных ручных reference-counted обертокReplacing uncontrolled, hand-rolled reference-counted wrappers

До 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.

03

Потокобезопасность самого счётчика без ручной синхронизацииThread-safety of the counter itself without manual synchronization

Копирование и уничтожение 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).

Как это работает под капотомHow it works under the hood

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.

Своя реализацияImplementing it yourself

упрощённо — без weak_ptr, без enable_shared_from_thissimplified — no weak_ptr, no enable_shared_from_this

Ожидаемый на интервью объём: 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).

ГраблиGotchas

Циклические ссылки — утечка памятиCyclic references — a memory leak

Два объекта, держащие 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 blockmake_shared saves an allocation, but extends the lifetime of the control block's memory

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).

this как shared_ptr изнутри метода — не new shared_ptr(this)this as a shared_ptr from inside a method — not new shared_ptr(this)

Создание нового независимого 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().