Inteligentne wskaźniki C++ – std::weak_ptr

cze 8, 2026 | Język C++

W poprzednim wpisie dotyczącym inteligentnych wskaźników zajmowaliśmy się std::unique_ptr oraz std::shared_ptr. Choć oba te typy wyręczają nas w zarządzaniu pamięcią, shared_ptr ma pewien mankament – może tworzyć cykle odniesień, przez które zasoby nigdy nie zostaną zwolnione. Właśnie do radzenia sobie z tym wyzwaniem służy std::weak_ptr.

Czym jest std::weak_ptr?

std::weak_ptr to tzw. słaby wskaźnik — obserwuje obiekt zarządzany przez shared_ptr, ale nie zwiększa licznika referencji. Oznacza to, że samo istnienie weak_ptr nie przedłuża życia obiektu. Gdy wszystkie shared_ptr do danego obiektu wygasną, obiekt zostanie zniszczony — niezależnie od tego, ile wskaźników weak_ptr na niego wskazuje.

weak_ptr nie umożliwia bezpośredniej interakcji z obiektem, którym zarządza. Korzystanie z niego wymaga uprzedniego wygenerowania shared_ptr przy użyciu funkcji lock() — działanie to nie powiedzie się jednak, jeżeli dany obiekt został wcześniej usunięty.

Problem cyklicznych referencji

Wyobraź sobie sytuację, w której dwa obiekty tworzą cykliczną zależność, ponieważ wzajemnie wskazują na siebie przy pomocy inteligentnych wskaźników shared_ptr:

C++
#include <iostream>
#include <memory>

struct B; // deklaracja zapowiadająca

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

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

int main() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();

    a->ptr_do_b = b;  // A trzyma B
    b->ptr_do_a = a;  // B trzyma A — cykl!

    // Koniec main() — oba obiekty NIE zostaną zniszczone!
    // Destruktory ~A i ~B nigdy się nie wywołają.
}

Gdy kończy się main(), lokalne zmienne a i b wychodzą z zakresu. Licznik referencji a spada do 1 (wciąż trzyma go b->ptr_do_a), a licznik b spada do 1 (wciąż trzyma go a->ptr_do_b). Żaden z nich nie osiąga zera — oba obiekty wiszą w pamięci na zawsze. To klasyczny wyciek pamięci spowodowany cyklem referencji.

ShellScript
(brak wyjścia  destruktory się nie wywołują)

Rozwiązanie: std::weak_ptr przerywa ten cykl

Wystarczy zamienić jedno z wzajemnych połączeń na inteligentny wskaźnik typu `weak_ptr`, co pozwala skutecznie przerwać cykl zależności blokujący poprawne zwalnianie pamięci. W takiej architekturze obiekt pełniący rolę „obserwującego” korzysta z `weak_ptr`, dzięki czemu nie zwiększa licznika silnych referencji, natomiast „właściciel” zasobu nadal bezpiecznie używa `shared_ptr` do zarządzania czasem życia obiektu:

C++
#include <iostream>
#include <memory>

struct B;

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

struct B {
    std::weak_ptr<A> ptr_do_a;  // weak_ptr zamiast shared_ptr!
    ~B() { std::cout << "~B\n"; }
};

int main() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();

    a->ptr_do_b = b;
    b->ptr_do_a = a;

    // Koniec main():
    // licznik a spada do 0 → ~A() się wywołuje
    // licznik b spada do 0 → ~B() się wywołuje
}

Teraz juz oba destruktory się wywołują. weak_ptr nie uczestniczy w liczniku referencji i cykl zostaje przerwany.

ShellScript
~A
~B

Jak używać weak_ptr — lock() i expired()

Ponieważ obiekt obserwowany przez weak_ptr może zostać zniszczony w dowolnym momencie, przed użyciem należy sprawdzić, czy wciąż istnieje. Do tego służą dwie metody:

  • expired() — zwraca true, jeśli obiekt już nie istnieje
  • lock() — próbuje utworzyć shared_ptr; jeśli obiekt nie istnieje, zwraca pusty shared_ptr
C++
#include <iostream>
#include <memory>

int main() {
    std::weak_ptr<int> slaby;

    {
        auto silny = std::make_shared<int>(42);
        slaby = silny;  // weak_ptr obserwuje obiekt

        if (auto ptr = slaby.lock()) {
            std::cout << "Wartość: " << *ptr << "\n";  // 42
        }
    }
    // silny wyszedl z zakresu — obiekt zniszczony

    if (slaby.expired()) {
        std::cout << "Obiekt juz nie istnieje!\n";
    }

    if (auto ptr = slaby.lock()) {
        std::cout << "Mamy dostep\n";
    } else {
        std::cout << "Brak dostepu — obiekt zniszczony\n";
    }
}

Zawsze używaj metody lock() zamiast bezpośredniego dereferencjonowania — to jedyna bezpieczna metoda dostępu do obiektu przez weak_ptr. Pozwala ona na uzyskanie tymczasowego shared_ptr, co gwarantuje, że zasób nie zostanie usunięty w trakcie jego używania i umożliwia sprawne zweryfikowanie, czy obiekt wciąż istnieje w pamięci.

ShellScript
Wartość: 42
Obiekt już nie istnieje!
Brak dostępu  obiekt zniszczony

Kiedy używać weak_ptr?

Stosuj std::weak_ptr w sytuacjach, gdy potrzebujesz dostępu do obiektu bez przejmowania nad nim współwłasności oraz w celu skutecznego rozbijania cykli referencji.

  • Chcesz przerwać cykl referencji między obiektami powiązanymi przez shared_ptr
  • Implementujesz cache lub rejestr obiektów — chcesz trzymać referencję, ale nie przedłużać życia obiektu
  • Budujesz relację obserwator–podmiot (wzorzec Observer) — obserwator nie powinien przedłużać życia podmiotu
  • Masz struktury drzewiaste z powrotnymi wskaźnikami — węzeł zna swojego rodzica przez weak_ptr, a dzieci przez shared_ptr

Podsumujmy

std::weak_ptr to niezbędne uzupełnienie shared_ptr wszędzie tam, gdzie obiekty mogą się wzajemnie obserwować. Nie zarządza własnością, jedynie obserwuje. Dzięki temu pozwala unikać cyklicznych zależności, które są jedną z najczęstszych pułapek przy pracy ze shared_ptr. Razem unique_ptr, shared_ptr i weak_ptr tworzą kompletny zestaw narzędzi do bezpiecznego zarządzania pamięcią w nowoczesnym C++.

Więcej o zarządzaniu pamięcią w C++ przeczytasz w artykule Podstawy języka C++ – część 4 – zarządzanie pamięcią.

PlatformIO – pierwszy projekt Arduino

PlatformIO – pierwszy projekt Arduino

Skoro mamy już zainstalowane środowisko Visual Studio Code z rozszerzeniem PlatformIO, możemy przejść do kolejnego etapu. Jesteśmy gotowi, aby przystąpić do utworzenia pierwszego projektu, wybierając odpowiedni model...

czytaj dalej