Close Menu
    Ciekawe

    Jak podłączyć telefon do monitora? Przewodowe i bezprzewodowe sposoby

    2025-12-08

    Co można wrzucić w koszty firmy jednoosobowej? Lista i praktyczne przykłady

    2025-12-03

    Jak podłączyć okulary VR do PS4? Poradnik podłączenia i konfiguracji

    2025-12-02
    Facebook X (Twitter) Instagram
    CPP Polska
    Facebook X (Twitter) Instagram
    • Biznes

      Co można wrzucić w koszty firmy jednoosobowej? Lista i praktyczne przykłady

      2025-12-03

      Jak zapobiec wyciekom danych firmowych?

      2025-11-28

      Ile kosztuje prowadzenie jednoosobowej działalności gospodarczej? Przegląd opłat

      2025-11-10

      Jak wziąć samochód w leasing bez firmy? Poradnik dla osób fizycznych

      2025-10-29

      Jak założyć firmę jednoosobową krok po kroku – koszty, formalności i czas trwania

      2025-10-23
    • Technologie

      Jak podłączyć telefon do monitora? Przewodowe i bezprzewodowe sposoby

      2025-12-08

      Jak podłączyć okulary VR do PS4? Poradnik podłączenia i konfiguracji

      2025-12-02

      Jak zapobiec wyciekom danych firmowych?

      2025-11-28

      Jak sprawdzić rozdzielczość monitora w Windows i macOS?

      2025-11-26

      Jak zresetować laptopa Acer do ustawień fabrycznych? Poradnik krok po kroku

      2025-11-25
    • Programowanie

      Maszyna stanów oparta o std::variant

      2025-10-07

      std::deque w C++ – kiedy wybrać dwukierunkową kolejkę zamiast vectora

      2025-10-07

      Tablice w C++ od podstaw – deklaracja, inicjalizacja, iteracja i typowe pułapki

      2025-10-07

      itoa i std::to_chars – konwersja liczb na tekst bez narzutu wydajności

      2025-10-07

      strcpy vs strncpy vs std::string – bezpieczne kopiowanie łańcuchów w C++

      2025-10-07
    • Inne

      Jak prowadzić blog programistyczny i dzielić się wiedzą?

      2025-06-28
    CPP Polska
    Home»C++»std::shared_ptr w praktyce – cykle referencji, weak_ptr i custom deleter
    C++

    std::shared_ptr w praktyce – cykle referencji, weak_ptr i custom deleter

    Oskar KlimkiewiczBy Oskar KlimkiewiczBrak komentarzy7 Mins Read
    Share Facebook Twitter LinkedIn Email Copy Link
    Follow Us
    RSS
    laptop compute displaying command prompt
    Share
    Facebook Twitter LinkedIn Email Copy Link

    Circumventing circular references: zaawansowane zastosowania std::shared_ptr z std::weak_ptr i niestandardowymi deleterami

    std::shared_ptr rewolucjonizuje zarządzanie zasobami w C++ poprzez umożliwienie semantyki współdzielonej własności za pomocą zliczania referencji. Jednak nieprawidłowe użycie prowadzi do istotnych problemów. Cyklowe zależności powstają, gdy wzajemnie referencyjne instancje std::shared_ptr tworzą zamknięte pętle własności, uniemożliwiając zwolnienie zasobu i prowadząc do wycieków pamięci. Niniejsza analiza przedstawia praktyczne strategie rozwiązywania takich cykli za pomocą std::weak_ptr oraz opisuje implementacje niestandardowych deleterów dla niekonwencjonalnych scenariuszy czyszczenia zasobów. Uwzględniając mechanizmy kompilatora, kompromisy wydajnościowe oraz przykłady z rzeczywistych projektów, przedstawiamy kompleksowe wytyczne dotyczące bezpiecznego stosowania inteligentnych wskaźników w złożonych systemach.

    Anatomia błędów związanych z cyklicznymi referencjami

    Mechanizmy zliczania referencji

    std::shared_ptr posiada blok kontrolny z dwoma licznikami: licznik referencji silnych (dla współwłasności) oraz słaby licznik referencji (dla instancji obserwatorów). Przy kopiowaniu lub przenoszeniu wskaźnika dzielonego licznik silny rośnie; zniszczenie dekrementuje go. Zasób jest zwalniany, gdy licznik silny osiągnie zero, niezależnie od liczby słabych referencji. Ten model zawodzi w przypadku powstania cykli własnościowych — każdy obiekt oczekuje na zwolnienie przez drugi, co skutkuje zablokowaniem zasobów w pamięci.

    Redukcyjne i wielowęzłowe cykle

    Minimalny cykl pojawia się, gdy obiekt zawiera shared_ptr do samego siebie:

    
    class Resource {
    public:
        std::shared_ptr<Resource> m_ptr; // Wskaźnik do samego siebie
        // Konstruktor/destruktor pominięte
    };
    int main() {
        auto ptr1 = std::make_shared<Resource>();
        ptr1->m_ptr = ptr1; // Cykl własności
    } // Resource nigdy nie zostanie zwolniony
    

    Instancja Resource przechowuje wewnętrzny m_ptr, który współdzieli własność z ptr1, co powoduje, że licznik referencji pozostaje na poziomie 1 po zniszczeniu ptr1. Bardziej złożone cykle dotyczą współzależnych obiektów:

    
    struct Node {
        std::shared_ptr<Node> next; // Łańcuch tworzy cykl
    };
    auto nodeA = std::make_shared<Node>();
    auto nodeB = std::make_shared<Node>();
    nodeA->next = nodeB; // A posiada B
    nodeB->next = nodeA; // B posiada A: cykl
    

    W tym przypadku zniszczenie nodeA i nodeB pozostawia ich liczniki referencji silnych na poziomie 1, wyciekając oba węzły. Tego typu wzorce często pojawiają się w implementacjach obserwatorów, dwukierunkowych listach czy hierarchiach rodzic-dziecko, gdzie logiczna wydaje się wzajemna własność.

    Analiza objawów

    Cykliczne referencje nie powodują bezpośrednio awarii aplikacji, lecz skutkują stopniowym ubytkiem pamięci. Narzędzia takie jak Valgrind czy sanitarizery adresów wykrywają „na pewno utracone” bloki równe rozmiarom cykli. Może wystąpić spadek wydajności, gdy obiekty gromadzą się w pamięci, choć wykrycie problemu często wymaga zainstalowania dodatkowego monitoringu destruktorów lub referencji.

    std::weak_ptr: przerywanie łańcuchów cyklicznych

    Semiotyka obserwatora

    std::weak_ptr obserwuje zasoby zarządzane przez shared_ptr, nie wpływając na własność. Powiązany jest z tym samym blokiem kontrolnym, lecz nie zmienia licznika silnych referencji. W ten sposób omija cykliczne blokady, nie podtrzymując sztucznie istnienia zasobu:

    
    struct Person {
        std::weak_ptr<Person> partner; // Bez własności
        // …
    };
    void partnerUp(std::shared_ptr<Person> p1,
                   std::shared_ptr<Person> p2) {
        p1->partner = p2; // Przypisanie przez weak_ptr
        p2->partner = p1;
    } // po wyjściu p1/p2: licznik silny spada do 0
    

    Destrukcja p1 i p2 nie jest blokowana przez wewnętrzne weak_ptr, bo słabe referencje nie wpływają na utrzymanie zasobu. Blok kontrolny istnieje dopóki przetrwają jakiekolwiek weak lub shared_ptr.

    Bezpieczny dostęp do zasobów

    Dostęp do obiektu weak_ptr wymaga promocji do shared_ptr poprzez lock():

    
    if (auto sharedPartner = p1->partner.lock()) {
        // Bezpieczny dostęp do sharedPartner
    } else {
        // Zasób już zwolniony
    }
    

    lock() atomowo sprawdza licznik silnych referencji: jeśli >0, konstruuje shared_ptr i inkrementuje licznik; jeśli 0, zwraca null. Dzięki temu nie występuje niebezpieczeństwo dostępu do zwolnionego zasobu, choć pojawia się narzut czasowy. Dla optymalizacji można użyć expired(), które tylko sprawdza stan zasobu.

    Długowieczność bloku kontrolnego

    Blok kontrolny trwa dłużej niż zarządzany zasób — do zwolnienia wszystkich weak_ptr. Pozwala to na:

    • Buforowanie – tymczasowe przechowywanie kosztownych do utworzenia obiektów;
    • Mostki między systemami – referencje między podsystemami bez utrudniania zamknięcia;
    • Diagnostykę – śledzenie cyklu życia po zwolnieniu obiektu.

    Minusem jest nieznaczny narzut pamięci na metadane bloku kontrolnego.

    Niestandardowe deletery: rozszerzenie zarządzania zasobami

    Mechanika deletera

    std::shared_ptr deleguje czyszczenie zasobu do deletera — wywoływanego, gdy licznik silny osiągnie zero. Domyślnie wywołuje delete lub delete[], lecz niestandardowa logika obsługuje unikalne zasoby:

    
    void FileDeleter(FILE* fp) {
        if (fp) {
            fflush(fp);
            fclose(fp); // Opróżnienie buforów przed zamknięciem
        }
    }
    std::shared_ptr<FILE> sp(
        fopen("data.txt", "r"),
        FileDeleter // Własna czyszczenie
    );
    

    Deletery są typowo zamazane (type-erased) w shared_ptr i przechowywane w bloku kontrolnym. Pozwala to na heterogeniczne deletery bez wpływu na typ wskaźnika. Z kolei unique_ptr przyjmuje deleter jako parametr szablonu, wpływając na zgodność typów:

    
    // unique_ptr: deleter jako część typu
    std::unique_ptr<FILE, decltype(&FileDeleter)> up(
        fopen("log.txt", "w"),
        FileDeleter
    );
    // shared_ptr: deleter zamazany typowo
    std::shared_ptr<FILE> sp(
        fopen("config.cfg", "r"),
        FileDeleter
    );
    

    Zakres zastosowań

    Deletery są niezbędne do:

    • API w C – dla zasobów SDL (SDL_FreeSurface), tekstur OpenGL (glDeleteTextures) czy uchwytów bazy danych;
    • Pooli obiektów – zamiast usuwania pozwala na szybkie zwroty do puli;
    • Audytu – logowanie zdarzeń usuwania dla celów diagnostycznych;
    • Interfejsów sprzętowych – zwalnianie blokad sprzętowych przed usunięciem pamięci.

    Kompromisy konstrukcyjne

    W przeciwieństwie do make_shared, niestandardowe deletery wymagają jawnego przydzielenia zasobów:

    
    auto p = std::make_shared<Resource>(); // Pojedynczy przydział
    // Niestandardowy deleter – oddzielne przydziały:
    std::shared_ptr<Resource> sp(
        new Resource(),
        [](Resource* r) { /*…*/ }
    ); // Osobno zasób i blok kontrolny
    

    Powoduje to większą fragmentację, ale jest konieczne dla niestandardowego zwalniania. Wariant allocate_shared obsługuje własne alokatory, co komplikuje użycie.

    Optymalizacja wydajności i bezpieczeństwa

    Analiza opłacalności

    shared_ptr wprowadza mierzalne narzuty:

    • Pamięć – ok. 16–32 bajty (2 wskaźniki: obiekt + blok kontrolny),
    • Operacje atomowe – inkrementacja/dekrementacja liczników referencji musi być wątkowo bezpieczna,
    • Przydział bloku kontrolnego – osobno od zarządzanego zasobu.

    make_shared łączy zasób i blok kontrolny w jednym przydziale, zmniejszając narzut i poprawiając lokalność danych. Z kolei niestandardowe deletery uniemożliwiają użycie make_shared, wymagając analizy korzyści specjalistycznego czyszczenia.

    Wątrobiezpieczeństwo

    shared_ptr gwarantuje:

    • atomowe aktualizacje liczników referencji,
    • bezpieczne kopiowanie i niszczenie w wielu wątkach,

    ale nie zapewnia wątkowego bezpieczeństwa dostępu do zarządzanego obiektu. Przykład:

    
    // Wątrobiezpieczne niszczenie:
    void share(std::shared_ptr<Resource> sp) {
        std::thread t([sp] { /* … */ }); // sp kopiowane bezpiecznie
    }
    // NIEBEZPIECZNY dostęp do danych obiektu:
    sp->data = 42; // Wymaga zewnętrznej synchronizacji
    

    Promocja weak_ptr (lock()) synchronizuje się z destruktorami, zapobiegając wyścigom i użyciu zwolnionych zasobów.

    Ryzyka inwersji własności

    Przekazywanie własności w wątkach może skutkować zakleszczeniem:

    
    struct Timer {
        std::jthread thread;
        ~Timer() { thread.join(); } // Czeka na zakończenie w destruktorze
    };
    struct App {
        std::shared_ptr<Timer> timer;
        void schedule() {
            timer->thread = std::jthread([s=self] {
                auto t = s->timer; // Deadlock: App posiada Timer
            });
        }
    };
    // App posiada Timer, wątek Timer posiada App: cykl
    

    Rozwiązanie: użycie weak_ptr w lambdzie wątku:

    
    timer->thread = std::jthread([w=weak_ptr<App>(this)] {
        if (auto s = w.lock()) {
            // Bezpieczne użycie s->timer
        }
    });
    

    Zalecenia modernizacyjne

    Minimalizacja cykli referencji

    1. Zamiana własności na obserwację – przekształć dwukierunkowe shared_ptr w jednokierunkowe weak_ptr, gdzie to możliwe;
    2. Użycie słabych wskaźników dla cache’ów – zapobiegnie wyciekom, gdy klienci przechowują referencje;
    3. Rozbijanie hierarchii – przekształć kod, by unikać wzajemnej własności (np. wzorzec mediatora);
    4. Automatyczne wykrywanie cykli – zastosuj deletery logujące niezwolnione obiekty.

    Niestandardowe wdrożenia

    • Wrappery do starszych API – deletery umożliwiają unowocześnienie zasobów z C:
    
    struct LegacyList { void ReleaseElements(); /*…*/ };
    auto deleter = [](LegacyList* p) {
        p->ReleaseElements();
        delete p;
    };
    std::shared_ptr<LegacyList> sp(new LegacyList, deleter);
    
    • Wzorce strażników – dołączanie diagnostycznych deleterów w debugowaniu:
    
    #ifdef DEBUG
    auto deleter = [](Resource* r) {
        logDestruction(r);
        delete r;
    };
    #endif
    

    Unikanie antywzorców

    • Unikaj konwersji na surowy wskaźnik – get() osłabia monitorowanie własności:
    
    auto sp = std::make_shared<int>(42);
    int* raw = sp.get();
    // Po zniszczeniu sp, raw staje się niepoprawny
    
    • Preferuj make_shared – eliminuje osobne przydziały i luki w obsłudze wyjątków:
    
    f(std::shared_ptr<Object>(new Object), g());
    // Jeśli g() rzuci wyjątkiem, Object wycieknie
    
    • Nigdy nie przechowuj this jako shared_ptr – stosuj enable_shared_from_this dla klas samoreferencyjnych.

    Podsumowanie: ku systemom odpornym na wycieki

    std::shared_ptr upowszechnia własność zasobów, ale wymaga czujności względem cyklicznych zależności. Synergia z std::weak_ptr pozwala rozwiązać te problemy, rozdzielając obserwację od własności i umożliwiając złożone topologie bez ryzyka wycieków. Niestandardowe deletery rozszerzają zarządzanie zasobami poza pamięć, łącząc współczesność C++ z zasobami zewnętrznymi lub sprzętowymi. Zastosowanie make_shared, atomowych promocji weak oraz diagnostycznych deleterów buduje fundament pod skalowalne, łatwe do utrzymania systemy zarządzania zasobami. Dalsze udoskonalenia obejmują integrację inteligentnych wskaźników z czasem życia korutyn czy wykorzystanie sprzętowych operacji atomowych pod dużym obciążeniem.


    Aneks implementacyjny: Rozwiązanie zredukowanego cyklu
    Korekta wycieku samoreferencyjnego Resource wymaga przerwania cyklu z zewnątrz. Ponieważ main() nie ma dostępu do m_ptr po zniszczeniu ptr1, należy zainicjować reset przed destrukcją:

    
    class Resource {
    public:
        std::shared_ptr<Resource> m_ptr;
        void resetPtr() { m_ptr.reset(); } // Przerwanie cyklu
        // ...
    };
    int main() {
        auto ptr1 = std::make_shared<Resource>();
        ptr1->m_ptr = ptr1;
        ptr1->resetPtr(); // Reset przed zniszczeniem
    } // Resource zwolniony
    

    To ręczne działanie przerywa cykl i pozwala licznikowi referencji spaść do zera. Wersje produkcyjne powinny kapsułkować podobną logikę w klasach strażników lub obiektach RAII, automatyzując proces rozbijania cykli.

    Polecane:

    • Kompendium wiedzy o smart-pointerach w C++
    • RAII w C++ – zbiór najlepszych praktyk
    • RAII i obsługa wyjątków – zarządzanie zasobami w C++
    • Semantyka przenoszenia i std::move – zarządzanie zasobami w C++
    • calloc, realloc i free – zarządzanie pamięcią w stylu C w nowoczesnym projekcie C++
    Share. Facebook Twitter LinkedIn Email Copy Link
    Oskar Klimkiewicz
    • Website

    Inżynier oprogramowania specjalizujący się w C++, absolwent Wydziału Elektroniki i Technik Informacyjnych Politechniki Warszawskiej. Od ponad 8 lat projektuje i rozwija systemy o wysokiej dostępności, głównie dla branży fintech i IoT. PS. Zdjęcie wyretuszowane przez AI :)

    Podobne artykuły

    Maszyna stanów oparta o std::variant

    8 Mins Read

    Tablice w C++ od podstaw – deklaracja, inicjalizacja, iteracja i typowe pułapki

    4 Mins Read

    std::deque w C++ – kiedy wybrać dwukierunkową kolejkę zamiast vectora

    4 Mins Read
    Leave A Reply Cancel Reply

    Oglądaj, słuchaj, ćwicz - zdobywaj nowe umiejętności online
    Nie przegap

    Jak podłączyć telefon do monitora? Przewodowe i bezprzewodowe sposoby

    Oskar Klimkiewicz6 Mins Read

    Podłączenie telefonu do monitora to jedna z najistotniejszych innowacji ery mobilnej, umożliwiająca przeniesienie doświadczeń z…

    Co można wrzucić w koszty firmy jednoosobowej? Lista i praktyczne przykłady

    2025-12-03

    Jak podłączyć okulary VR do PS4? Poradnik podłączenia i konfiguracji

    2025-12-02

    Jak zapobiec wyciekom danych firmowych?

    2025-11-28
    Social media
    • Facebook
    • Twitter
    • LinkedIn
    O nas
    O nas

    CPP Polska to serwis internetowy poświęcony technologii, programowaniu, IT, biznesowi i finansom. Znajdziesz tu porady, wskazówki i instrukcje dla wszystkich czytelników IT & Tech & Biz.

    Facebook X (Twitter) LinkedIn RSS
    Najnowsze

    Jak podłączyć telefon do monitora? Przewodowe i bezprzewodowe sposoby

    2025-12-08

    Co można wrzucić w koszty firmy jednoosobowej? Lista i praktyczne przykłady

    2025-12-03

    Jak podłączyć okulary VR do PS4? Poradnik podłączenia i konfiguracji

    2025-12-02
    Popularne

    Skrajnie niepotrzebne, skrajne przypadki w C++

    2025-06-28

    Wyszukiwanie testów w Google Test – metody i narzędzia

    2025-06-28

    Czy C jest wolniejszy od C++? Zero-cost abstraction w praktyce

    2025-06-28
    © 2025 CPP Polska. Wszelkie prawa zastrzeżone.
    • Lista publikacji
    • Współpraca
    • Kontakt

    Type above and press Enter to search. Press Esc to cancel.