← Назад к вопросам

Как предотвратить циклическую зависимость с помощью std::weak_ptr?

2.0 Middle🔥 241 комментариев
#Умные указатели и управление памятью

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI28 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

std::weak_ptr для предотвращения циклических зависимостей

Проблема циклических ссылок

struct Node {
    std::shared_ptr<Node> next;
};

auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();

node1->next = node2;  // node1 -> node2
node2->next = node1;  // node2 -> node1 (ЦИКЛ)

// Когда выходим из scope, оба объекта НЕ удаляются
// node1 имеет node2 на reference (ref_count = 2)
// node2 имеет node1 на reference (ref_count = 2)
// Оба говорят друг другу: "ты должен остаться для меня"
// Утечка памяти (memory leak)

Решение: std::weak_ptr

weak_ptr НЕ увеличивает reference count:

struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;  // Не увеличивает ref_count
};

auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();

node1->next = node2;      // node1 -> node2 (ref_count[node2] = 2)
node2->prev = node1;      // node2 -weak-> node1 (ref_count[node1] = 1)

// Когда выходим из scope:
// node1 удалится (ref_count[node1] = 0)
// Тогда node2 удалится (ref_count[node2] = 0)
// БЕЗ утечки

Как использовать weak_ptr

std::weak_ptr<Node> weakPtr = node1;

// Чтобы использовать, нужно преобразовать в shared_ptr
if (auto shared = weakPtr.lock()) {
    // объект ещё существует
    shared->process();
} else {
    // объект уже удалён
    std::cout << "Object was deleted\n";
}

Практический пример: двусвязный список

struct ListNode {
    int value;
    std::shared_ptr<ListNode> next;
    std::weak_ptr<ListNode> prev;  // Слабая ссылка на предыдущий
};

class DoublyLinkedList {
public:
    void append(int value) {
        auto newNode = std::make_shared<ListNode>();
        newNode->value = value;
        
        if (tail) {
            newNode->prev = tail;  // Слабая ссылка
            tail->next = newNode;
        } else {
            head = newNode;
        }
        tail = newNode;
    }
    
    void printForward() {
        auto node = head;
        while (node) {
            std::cout << node->value << " -> ";
            node = node->next;
        }
        std::cout << "null\n";
    }
    
    void printBackward() {
        auto node = tail;
        while (node) {
            std::cout << node->value << " <- ";
            if (auto prev = node->prev.lock()) {
                node = prev;
            } else {
                break;
            }
        }
        std::cout << "null\n";
    }
    
private:
    std::shared_ptr<ListNode> head;
    std::shared_ptr<ListNode> tail;
};

Observer паттерн с weak_ptr

class Observer {
public:
    virtual void update() = 0;
    virtual ~Observer() = default;
};

class Subject {
private:
    std::vector<std::weak_ptr<Observer>> observers;
    
public:
    void subscribe(std::shared_ptr<Observer> obs) {
        observers.push_back(obs);
    }
    
    void notify() {
        // Удаляем мёртвые ссылки
        auto it = std::remove_if(
            observers.begin(), observers.end(),
            [](const auto& obs) { return obs.expired(); }
        );
        observers.erase(it, observers.end());
        
        // Уведомляем живые
        for (auto& obs : observers) {
            if (auto shared = obs.lock()) {
                shared->update();
            }
        }
    }
};

Граф с циклами

struct GraphNode {
    int value;
    std::vector<std::shared_ptr<GraphNode>> children;
    std::weak_ptr<GraphNode> parent;  // Слабая ссылка на родителя
};

auto root = std::make_shared<GraphNode>();
auto child = std::make_shared<GraphNode>();

root->children.push_back(child);
child->parent = root;  // Слабая ссылка, без цикла

Когда использовать weak_ptr

✅ Используй weak_ptr для:

  • Parent-child отношений (child -> parent = weak)
  • Observer паттерна (subject не владеет observers)
  • Кеша (кеш не должен держать объекты живыми)
  • Обратных ссылок в графах

❌ Не используй weak_ptr:

  • Вместо shared_ptr основного владения
  • Если не нужны циклические ссылки
  • В простых случаях (используй просто значения)

Потенциальные проблемы

Race condition:

auto sharedPtr = weakPtr.lock();  // Получили shared_ptr
// Между lock() и использованием объект может быть удалён
if (sharedPtr) {
    sharedPtr->use();  // OK, объект существует
}

Забыли lock():

weakPtr->use();  // ❌ Ошибка компиляции! weak_ptr нельзя разыменовать напрямую

// Правильно:
if (auto sp = weakPtr.lock()) {
    sp->use();  // OK
}

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

// 1. Используй shared_ptr для основного владения
auto ptr = std::make_shared<Object>();

// 2. Используй weak_ptr для обратных ссылок
struct Parent { };
struct Child {
    std::weak_ptr<Parent> parent;
};

// 3. Всегда проверяй expired() или используй lock()
if (!weakPtr.expired()) {
    if (auto sp = weakPtr.lock()) {
        sp->use();
    }
}

// 4. Очищай мёртвые ссылки
observers.erase(
    std::remove_if(observers.begin(), observers.end(),
                   [](auto& w) { return w.expired(); }),
    observers.end()
);

Производительность

  • shared_ptr: быстро
  • weak_ptr: немного медленнее (extra bookkeeping)
  • Но намного быстрее чем утечка памяти

Заключение

  • shared_ptr для владения ресурсом
  • weak_ptr для неопасных ссылок
  • weak_ptr предотвращает утечки в циклических структурах
  • Всегда lock() перед использованием
  • Проверяй expired() перед операциями