C++11 #10: Inteligentne wskaźniki wchodzą do gry: std::unique_ptr.

|

Inteligentne wskaźniki wchodzą do gry.

Do tej pory wpisy na temat C++11 dotyczyły bardzo ważnych i przydatnych zmian w języku. Jednak to zarządzanie pamięcią i wskaźniki doczekały się największych, fundamentalnych usprawnień w nowym standardzie – jednym z najważniejszych są tzw. inteligentne wskaźniki.

Oczywiście nadal można ręcznie zarządzać pamięcią w C++ – to fundament języka. Nie wyklucza to jednak wprowadzania nowych rozwiązań, które przyczyniają się do drastycznych zmian w sposobie zarządzania życiem obiektów i jednocześnie eliminują większość paskudnych błędów.

Zmiana podejścia i rozwiązanie największych problemów.

Przed C++11 trzeba było bardzo uważać podczas stosowania dynamicznej alokacji pamięci i używania wskaźników. To było jak chodzenie po nieskończenie szerokim polu minowym – pytanie nigdy nie brzmiało „Czy ?”, ale „Kiedy ?”…

Problemy ze stosowaniem zwykłych wskaźników skupiały się zawsze wokół 3 zagadnień:

  1. Kto jest właścicielem obiektu, na który pokazuje wskaźnik ?
  2. Na co konkretnie wskazuje nasz wskaźnik ?
  3. Czy obiekt, na który wskazujemy, jest poprawny w momencie dostępu poprzez wskaźnik ?

Z nich wynikały wszelkiego rodzaju nieporozumienia, prowadzące do kolejnych, bardziej szczegółowych pytań:

  • Jak długo powinien żyć obiekt i kto ma o tym decydować ?
  • Kto i jak powinien zwolnić pamięć po obiekcie ?
  • Czy przypadkiem nie wskazujemy na tablicę obiektów, a nie na pojedynczy element ?
  • Czy pamięć, na którą wskazujemy, nie została już wcześniej zwolniona ?

No właśnie.

Wielka trójka.

W C++11 zostały zdefiniowane 3 inteligentne wskaźniki (ang. smart pointers):

  • std::unique_ptr
  • std::shared_ptr
  • std::weak_ptr

Celowo nie wymieniam tutaj, odziedziczonego po C++98, std::auto_ptr, bo cóż – lepiej o nim nie wspominać…

Wszystkie inteligentne wskaźniki to tak naprawdę klasy, które opakowują zwykłe wskaźniki, zapewniając wygodny interfejs, dużo większe możliwości i nieporównywalnie większy poziom bezpieczeństwa. Inteligentny wskaźnik zarządza przechowywanym obiektem w sposób kompleksowy – decyduje o czasie jego życia, w odpowiedni sposób zwalniając pamięć, kiedy obiekt nie jest już potrzebny.

Na początek porozmawiamy o klasie std::unique_ptr, natomiast dwa kolejne inteligentne wskaźniki omówimy sobie  w kolejnym wpisie.

std::unique_ptr.

std::unique_ptr realizuje ideę wyłącznej własności (ang. exclusive ownership). Oznacza to, iż jednocześnie powinien istnieć dokładnie jeden obiekt tego typu, wskazujący na konkretny obiekt w pamięci. Inaczej rzecz ujmując, std::unique_ptr zawsze jest wyłącznym właścicielem tego, na co wskazuje.

Onacza to, że:

  • nie można skopiować obiektu klasy std::unique_ptr
  • w momencie usunięcia (zniszczenia) obiektu klasy std::unique_ptr, wywoływany jest też destruktor obiektu przechowywanego wewnątrz
  • można dokonać przeniesienia (nie skopiowania !) obiektu, przechowywanego w jednym unikalnym wskaźniku, do innego unikalnego wskaźnika – wskaźnik źródłowy staje się wtedy wskaźnikiem pustym (jest to związane z tzw. semantyką przeniesienia, która zadebiutowała w C++11)

std::unique_ptr jest dostarczany w dwóch wersjach: jako std::unique_ptr<T> oraz std::unique_ptr<T[]>. W ten sposób unikamy sytuacji opisanej powyżej, kiedy nie wiemy, czy wskazujemy na pojedynczy obiekt, czy też na tablicę. Jeśli rzeczywiście chcemy przechowywać surową tablicę, musimy skorzystać z konkretnej wersji szablonu.

Jeśli chodzi o zastosowanie std::unique_ptr, można założyć, że w znakomitej większości przypadków powinien to być nasz domyślny wybór. Dzięki tej klasie możemy z powodzeniem zastąpić większość surowych wskaźników w naszych konstrukcjach, bez wyraźnej różnicy, jeśli chodzi o rozmiar i wydajność generowanego kodu.

Stosując std::unique_ptr zamiast surowego wskaźnika, możemy łatwo osiągnąć najważniejszy cel – raz na zawsze poradzić sobie z zarządzaniem dynamicznie alokowanymi obiekatami, bez martwienia się o zarządzanie pamięcią. Jeśli opuścimy zakres ważności unikalnego wskaźnika – zostanie on zniszczony wraz z przechowywanym obiektem, a pamięć zostanie poprawnie zwolniona.

Prosty przykład użycia std::unique_ptr.

void MyFunction()
{
    std::unique_ptr<int> IntPointer; /* obiekt jest pusty po wywołaniu 
                                      * domyślengo konstruktora
                                      */

    IntPointer.reset(new int(4));    /* zniszcz aktualnie przechowywany obiekt 
                                      * (jeśli nie nullptr) i przejmij własność obiektu,
                                      * na który wskazuje argument
                                      */
    // jeśli IntPointer nie jest pusty
    if (IntPointer) 
    {
        // wyświetl wartość przechowywanego obiektu
        std::cout << "Value: " << *IntPointer << '\n';
    }

    // wychodzimy z zakresu obiektu std::unique_ptr
    // zniszcz obiekt IntPointer i zwolnij pamięć po nim
}

Ten bardzo prosty i krótki kod pokazuje nam, jak intuicyjne jest użycie unikalnego wskaźnika.

Ze względu na wewnętrzną implementację (zaimplementowany operator bool()) możemy łatwo sprawdzić poprawność naszego wskaźnika – bez jawnego porównywania go z std::nullptr lub (o zgrozo !) z NULL.

Jeśli chodzi o dereferencję unikalnego wskaźnika w celu użycia przechowywanego obiektu, to tak, jak w przypadku surowych wskaźników, operatorem jest * . Sposób jego użycia jest identyczny.

Jeśli jest nam to potrzebne, możemy też oczywiście jawnie wydobyć przechowywany wewnątrz, surowy wskaźnik – w tym celu mamy do dyspozycji funkcje get oraz release. Pierwsza zwróci nam wskaźnik, za który nadal będzie odpowiadał obiekt klasy std::unique_ptr (czyli np. będzie on próbował zwolnić pamięć podczas destrukcji). Druga po zwróceniu przechowywanego zasobu unieważni unikalny wskaźnik (ustawi przechowywany wskaźnik na nullptr) – wtedy już sami odpowiadamy za zarządzanie uzyskanym obiektem.

Funkcja MyFunction nie zawiera kodu zwalniającego pamięć po utworzonym dynamicznie obiekcie typu int – dzięki std::unique_ptr wszystko dzieje się automagicznie.

Rozumiem, że powyższy, celowo prosty i sztuczny przykład, może nie robić wielkiego wrażenia. Jednak w sytuacji, w której z takiej funkcji można wyjść kilkoma ścieżkami – np. z kilku instrukcji warunkowych – zaczyna się robić ciekawie. Wersja z inteligentnym wskaźnikiem nie potrzebuje wtedy żadnych zmian. W wersji bez std::unique_ptr, w każdej możliwej ścieżce musimy umieścić kod, który poprawnie zwolni pamięć.

Ogólne założenie było takie, żeby z nowej klasy dało się korzystać podobnie, jak ze zwykłego wskaźnika – i tę prostotę w interfejsie udało się zachować. Dzięki temu przełączenie się na wskaźniki inteligentne nie jest tak trudne i przychodzi dość naturalnie. Nowe konstrukcje wymagają opanowania zaledwie kilku operatorów i prostych funkcji składowych.

Klasa std::unique_ptr nie jest zbyt rozbudowana – opis całego interfejsu można znaleźć TUTAJ.

Kontynuuj poznawanie C++11: C++11 #11: Inteligentne wskaźniki wchodzą do gry: std::shared_ptr, std::weak_ptr.

Dodaj komentarz