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ń:
- Kto jest właścicielem obiektu, na który pokazuje wskaźnik ?
- Na co konkretnie wskazuje nasz wskaźnik ?
- 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.