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:
#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.
(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:
#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.
~A
~BJak 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()— zwracatrue, jeśli obiekt już nie istniejelock()— próbuje utworzyćshared_ptr; jeśli obiekt nie istnieje, zwraca pustyshared_ptr
#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.
Wartość: 42
Obiekt już nie istnieje!
Brak dostępu — obiekt zniszczonyKiedy 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 przezshared_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ą.


