Перейти к содержимому

Shared ptr, weak ptr & Ownership

Поймите, что такое совместное владение с shared ptr, разбейте циклы со слабым ptr, узнайте о блоках управления и псевдонимах и узнайте, когда shared ptr является неправильным выбором.

Shared ptr: общая собственность

std::shared_ptr обеспечивает совместное владение — несколько экземпляров shared_ptr могут владеть одним и тем же объектом. Объект уничтожается при уничтожении или сбросе last, владеющего shared_ptr.

Внутри shared_ptr поддерживает блок управления **, содержащий:
сильное количество ссылок (количество владельцев shared_ptr)
Слабое количество ссылок (количество наблюдателей weak_ptr)
- Удалитель и распределитель

shared_ptr больше, чем необработанный указатель (обычно 2 указателя: один на объект, один на блок управления) и подсчет ссылок имеет атомные накладные расходы на каждую копию / разрушение. Используйте его только тогда, когда собственность действительно разделена.

make shared и подсчёт ссылок

std::make_shared(args...) выделяет объект и блок управления в одном распределении (вместо двух). Это более удобно для кэш-памяти и позволяет избежать проблемы безопасности сырых new.

shared_ptr_basics.cpp
#include <iostream>
#include <memory>
#include <string>

struct Logger {
    std::string name;
    Logger(std::string n) : name(std::move(n)) {
        std::cout << name << " created\n";
    }
    ~Logger() { std::cout << name << " destroyed\n"; }
    void log(const std::string& msg) {
        std::cout << "[" << name << "] " << msg << '\n';
    }
};

int main() {
    auto logger = std::make_shared<Logger>("AppLogger");
    std::cout << "use_count: " << logger.use_count() << '\n';  // 1

    {
        auto copy = logger;  // shared ownership
        std::cout << "use_count: " << logger.use_count() << '\n';  // 2
        copy->log("inside scope");
    }  // copy destroyed, count drops to 1

    std::cout << "use_count: " << logger.use_count() << '\n';  // 1
    logger->log("still alive");
}  // logger destroyed — count drops to 0, Logger deleted

Обсуждение weak ptr: Breaking Cycles

std::weak_ptr наблюдает объект под управлением shared_ptr без продления срока его службы. Чтобы получить доступ к объекту, позвоните в lock(), который возвращает shared_ptr или nullptr, если объект уже был уничтожен. weak_ptr необходим для взлома круговых ссылок , которые в противном случае могли бы просочиться.

weak_ptr_example.cpp
#include <iostream>
#include <memory>
#include <string>

struct Employee;
struct Team {
    std::string name;
    std::vector<std::shared_ptr<Employee>> members;
    Team(std::string n) : name(std::move(n)) {}
    ~Team() { std::cout << "Team " << name << " destroyed\n"; }
};

struct Employee {
    std::string name;
    std::weak_ptr<Team> team;  // weak — does NOT own the team
    Employee(std::string n) : name(std::move(n)) {}
    ~Employee() { std::cout << "Employee " << name << " destroyed\n"; }

    void print_team() {
        if (auto t = team.lock()) {  // try to get shared_ptr
            std::cout << name << " belongs to " << t->name << '\n';
        } else {
            std::cout << name << " has no team\n";
        }
    }
};

int main() {
    auto team = std::make_shared<Team>("Engineering");
    auto alice = std::make_shared<Employee>("Alice");
    auto bob = std::make_shared<Employee>("Bob");

    team->members.push_back(alice);
    team->members.push_back(bob);
    alice->team = team;  // weak_ptr — no circular ownership
    bob->team = team;

    alice->print_team();  // "Alice belongs to Engineering"

    team.reset();  // destroy team
    alice->print_team();  // "Alice has no team" — weak_ptr expired
}
Подводный камень

Если два объекта держат shared_ptr друг с другом, ни один из них не может достичь нулевого отсчета — они сохраняют друг друга живыми навсегда. Это утечка памяти, даже если вы используете умные указатели.

struct Node {
    std::shared_ptr<Node> next;  // if next points back, LEAK!
};
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->next = a;  // circular — neither a nor b will ever be destroyed

Исправление: сделайте одно направление weak_ptr. В древесных структурах дети держат weak_ptr перед родителями (или необработанными указателями наблюдателей). В двойных списках используйте weak_ptr для указателя prev.

Конструктор отчуждения и включите shared from this

Конструктор создает shared_ptr, который разделяет право собственности с существующим shared_ptr, но указывает на другой объект ** (обычно член). Объект с псевдонимом остается в живых до тех пор, пока существует любой псевдоним shared_ptr:

struct Pair { int first; int second; };
auto p = std::make_shared<Pair>(Pair{1, 2});
std::shared_ptr<int> sp_first(p, &p->first);  // shares p's control block
// p can go out of scope — Pair stays alive because sp_first still references it

enable_shared_from_this является базовым классом CRTP, который позволяет объекту, управляемому shared_ptr, получить shared_ptr для себя через shared_from_this(). Без него создание нового shared_ptr от this создает второй блок управления , что приводит к двойному освобождению.

Лучшая практика

shared_ptr перегружен. Достигните его только тогда, когда собственность действительно разделена в течение независимых жизней. Не используйте shared_ptr в качестве ленивой замены для мышления о собственности.

Избегайте shared_ptr, когда:
Единственный владелец существует - используйте вместо этого unique_ptr (ноль накладных расходов).
Объект живет на стеке — просто используйте объект напрямую.
Вам просто нужно сдать рекомендацию — сдать T& или T*.
Вы используете его «потому что он безопасен» — shared_ptr имеет временную стоимость (атомный обратный счет, дополнительное распределение) и скрывает семантику владения.
В критичных для производительности путях операции по пересчету атомной энергии стоят на удивление дорого.

Легитимные сценарии совместной собственности: кэши (множественные поиски возвращают один и тот же объект), системы паб / суб, объекты общей конфигурации и структуры данных графов с действительно общими узлами.

Контрольный блок Internals

Понимание блока управления помогает объяснить стоимость shared_ptr. make_shared объединяет объект и блок управления в одно распределение. Построение из сырого new требует двух отдельных выделений.

control_block.cpp
#include <memory>
#include <iostream>

struct Widget {
    int data[4];
};

int main() {
    // Two allocations: one for Widget, one for control block
    std::shared_ptr<Widget> p1(new Widget{});

    // Single allocation: Widget + control block together
    auto p2 = std::make_shared<Widget>();

    // Sizes (typical 64-bit platform)
    std::cout << "sizeof(Widget*):          " << sizeof(Widget*) << '\n';   // 8
    std::cout << "sizeof(unique_ptr<Widget>): " << sizeof(std::unique_ptr<Widget>) << '\n';  // 8
    std::cout << "sizeof(shared_ptr<Widget>): " << sizeof(std::shared_ptr<Widget>) << '\n';  // 16

    // shared_ptr is 2 pointers: one to object, one to control block
    // Control block contains: strong count, weak count, deleter, allocator

    std::cout << "p2 use_count: " << p2.use_count() << '\n';  // 1
    std::weak_ptr<Widget> w = p2;
    std::cout << "p2 use_count: " << p2.use_count() << '\n';  // still 1
    // weak_ptr does NOT increase strong count
}
Ключевые выводы
  • shared_ptr использует подсчет ссылок - объект умирает, когда последний владелец ушел
  • make_shared выделяет объект + блок управления в одном распределении (предпочитает всегда).
  • weak_ptr наблюдает без владения — использует lock() для безопасного доступа, нарушает круговые ссылки
  • shared_ptr имеет ширину 2 указателя и накладные расходы на атомный пересчет - не бесплатно
  • По умолчанию unique_ptr; используйте shared_ptr только тогда, когда право собственности действительно совместное.
  • Никогда не создавайте два shared_ptr из одного и того же исходного указателя - используйте enable_shared_from_this для самоссылок

Тест — проверьте свои знания

(15 XP)

1. Что происходит, когда два объекта удерживают `shared_ptr` друг с другом?

2. Почему `std::make_shared` предпочтительнее `shared_ptr<T>(new T(...))`?

3. Как безопасно получить доступ к объекту через `weak_ptr`?