Inteligentne wskaźniki - Smart Pointers¶
Mechanizm RAII¶
Mechanizm wyjątków a zasoby¶
Używanie natywnych wskaźników (raw pointers) do zarządzania zasobami może powodować wycieki zasobów.
void use_resource()
{
Resource* rsc = new Resource(); // resource acquisition
rsc->use(); // use() may throw
may_throw();
delete rsc; // resource release
}
W celu zabezpieczenia przed wyciekiem zasobu możemy użyć konstrukcji try-catch
:
// Nieudolna poprawa
void use_resource()
{
Resource* rsc = nullptr;
try
{
rsc = new Resource();
rsc->use(); // Kod, który używa rsc i może rzucić wyjątkiem
may_throw();
}
catch(...) //Przechwytuje wszystkie wyjątki
{
delete rsc;
throw;
}
delete rsc;
}
Niestety w rezultacie kod staje się mało czytelny i występuje w nim duplikacja zwalniania zasobu - delete rsc
.
Zarządzanie zasobami RAII¶
Resource Acquisition Is Initialization (RAII) - zdobywanie zasobów jest inicjowaniem. Technika łączy przejęcie i zwolnienie zasobu z inicjalizacją zmiennych lokalnych i ich automatyczną deinicjalizacją. Przejęcie zasobu jest połączone z konstrukcją, a zwolnienie z automatyczną destrukcją obiektu. Ponieważ wywołanie destruktora jest automatyczne gdy zmienna wyjdzie poza swój zasięg, jest zagwarantowane, że zasób zostanie zwolniony od razu gdy skończy się czas życia zmiennej. Mechanizm ten działa również przy wystąpieniu wyjątku. RAII jest kluczową koncepcją przy pisaniu kodu odpornego na wycieki zasobów.
std::mutex mtx;
int state = 0;
void update_state(); // updates state
void unsafe_code()
{
mtx.lock();
update_state(); // may throw
mtx.unlock();
}
class lock_guard
{
std::mutex& mtx_;
public:
lock_guard(std::mutex& mtx) : mtx_{mtx}
{
mtx_.lock();
}
~lock_guard()
{
mtx_.unlock();
}
lock_guard(const lock_guard&) = delete;
lock_guard& operator=(const lock_guard&) = delete;
};
// using RAII object
void safe_code()
{
lock_guard<mutex> lk{mtx};
update_state(); // may throw
}
Kopiowanie obiektów RAII¶
Klasy implementujące RAII posiadają destruktor, dlatego należy określić sposób zachowania obiektów przy ich kopiowaniu. Możliwe są następujące strategie:
- Całkowite blokowanie kopiowania oraz transferu prawa własności
- Blokowanie kopiowania przy jednoczesbym umożliwieniu transferu prawa własności do zasobu
- Zezwolenie na kopiowanie obiektów - obiekty współdzielą prawo własności z wykorzystaniem licznika referencji
Inteligentne wskaźniki¶
Inteligentne wskaźniki umożliwiają wygodne zarządzanie obiektami, które zostały zaalokowane na stercie za pomocą operatora new
.
Przejmują odpowiedzialność za wywołanie destruktora dla zarządzanego obiektu oraz zwolnienie pamięci - poprzez wywołanie operatora delete
.
Inteligentne wskaźniki w C++11 umożliwiaja reprezentację prawa własności do zaalokowanego zasobu.
Przeciążając operatory operator*
oraz operator->
umożliwiają korzystanie z nich w taki sam sposób, jak wskaźników natywnych.
Dostępne implementacje inteligentnych wskaźników w bibliotece standardowej C++ oraz w bibliotece Boost.
Klasa wskaźnika | Kopiowanie | Transfer prawa własności | Licznik referencji |
---|---|---|---|
std::auto_ptr<T> |
o | o | |
std::unique_ptr<T> |
o | ||
std::shared_ptr<T> |
o | o | wewnętrzny |
boost::scoped_ptr<T> |
|||
boost::shared_ptr<T> |
o | o | wewnętrzny |
boost::intrusive_ptr<T> |
o | zewnętrzny |
Klasa std::unique_ptr<T>
¶
Plik nagłówkowy: <memory>
Klasa szablonowa std::unique_ptr
służy do zapewnienia właściwego usuwania przydzielanego dynamicznie obiektu.
Implementuje RAII - destruktor inteligentnego wskaźnika usuwa wskazywany obiekt. Wskaźnik unique_ptr
nie może być ani kopiowany ani przypisywany, może być jednakże przenoszony.
Przeniesienie prawa własności odbywa się zgodnie z move semantics w C++11 - wymaga dla referencji do l-value jawnego transferu przy pomocy funkcji std::move()
.
void f()
{
std::unique_ptr<Gadget> my_gadget {new Gadget()};
// kod, który może wyrzucać wyjątki
my_gadget->use();
std::unique_ptr<Gadget> your_gadget = std::move(my_gadget); // explicit move
} // Destruktor klasy unique_ptr wywoła operator delete dla wskaźnika
// do kontrolowanej instancji
Semantyka przenoszenia dla unique_ptr
¶
Obiekt std::unique_ptr
nie może być kopiowany. Ale dzięki semantyce przenoszenia, może być stosowany tam, gdzie zgodne ze standardem C++03 niekopiowalne obiekty nie mogły działać:
- może być zwracany z funkcji
std::unique_ptr<Gadget> create_gadget(int type)
{
auto gdgt = unique_ptr<Gadget> {new Gadget(arg)};
return gdgt;
}
auto ptr_gadget = create_gadget(1); // implicit move
ptr_gadget->do_something();
W C++14 funkcję create_gadget()
można zastąpić biblioteczną funkcją make_unique()
, która tranferuje swoje argumenty wywołania do konstruktora alokowanego na stercie obiektu:
auto ptr = make_unique<Gadget>(arg); // C++14
- może być przekazywany (przenoszony) do funkcji jako parametr sink
void sink(unique_ptr<Gadget> gdgt)
{
gdgt->call_method();
// sink takes ownership - deletes the object pointed by gdgt
}
// ...
sink(move(ptr)); // explicitly moving into sink
sink(create_gadget(2)); // implicit move
- może być przechowywany w kontenerach STL (w standardzie C++11)
vector<unique_ptr<Gadget>> gadgets;
gadgets.emplace_back(new Gadget());
gadgets.push_back(unique_ptr<Gadget>(new Gadget())); // implicit move to the container
gadgets.push_back(create_gadget(3)); // implicit move to the container
for(const auto& g : gadgets)
g->do_something();
gadgets.clear(); // elements are automatically destroyed
Wskaźniki klas pochodnych¶
Wskaźnik do klasy pochodnej może zostać przypisany do wskaźnika do klasy bazowej (upcasting). Daje to możliwość stosowania polimorfizmu z wykorzystaniem funkcji wirtualnych.
Gadget* pb = new SuperGadget();
pb->do_something();
Odpowiednik z użyciem unique_ptr
std::unique_ptr<Gadget> pb = std::make_unique<SuperGadget>();
//explicit conversion - hard to miss it
auto pb = std::unique_ptr<Gadget>{ std::make_unique<SuperGadget>() };
Dealokatory¶
Używając wskaźnika std::unique_ptr
można zdefiniować własny dealokator, który będzie odpowiedzialny za prawidłowe zwolnienie zasobu. Umożliwia to
kontrolę nad zasobami innymi niż obiekty dynamicznie alokowane na stercie lub wymagającymi specjalnej obsługi w fazie destrukcji.
Aby użyć własnego dealokatora należy podać jego typ jako drugi parametr szablonu std::unique_ptr<T, Dealloc>
oraz przekazać instancję dealokatora jako drugi parametr konstruktora:
{
std::unique_ptr<FILE, int(*)(FILE*)> file{fopen("test.txt"), &fclose};
read(file.get());
} // fclose() is called for an opened file
Ważne
Dealokator dla unique_ptr
wywoływany jest tylko, jeśli wewnętrzny wskaźnik jest rózny od nullptr
!
Idiom PIMPL¶
Wskaźnik std::unique_ptr
świetnie nadaje się do stosowania tam, gdzie wcześniej stosowane były wskaźniki zwykłe albo obiekty typu std::auto_ptr
(obecnie mający status deprecated), np. do implementacji idiomu PIMPL
PIMPL - Private Implementation:
- minimalizuje zależności na etapie kompilacji
- separuje interfejs od implementacji
- ukrywa implementację
Plik blob.hpp
:¶
class Blob
{
public:
// interfejs klasy
/* … */
~Blob(); // must be only declared
private:
class Impl; // deklaracja zapowiadająca
// implementacja jest ukryta
std::unique_ptr<Impl> pimpl_; // wskaźnik do implementacji
};
Plik blob.cpp
:¶
class Blob::Impl
{
// wszystkie składowe prywatne (pola i metody)
// zmiany implementacji nie wymagają rekompilacji klas obiektów korzystających
// z instancji Blob
};
Blob::Blob() : pimpl_ {new Impl()}
{
// ustawienie stanu obiektu implementującego
}
Blob::~Blob() = default; // important! after defintion of Blob::Impl()
Zastosowanie std::unique_ptr<T>
¶
Wskaźniki std::unique_ptr
należy stosować tam, gdzie:
- w zasięgu obarczonym ryzykiem zgłoszenia wyjątku występuje wskaźnik
- funkcja ma kilka ścieżek wykonania i kilka punktów powrotu
- istnieje tylko jeden obiekt zarządzający czasem życia alokowanego obiektu
- ważna jest odporność na wyjątki
Klasa std::unique_ptr<T[]>
¶
Szablon std::unique_ptr
stanowi również lepszą alternatywę dla klasycznych tablic przydzielanych dynamicznie.
Przejmuje zarządzanie czasem życia takich tablic.
Oferuje przeciążony operator indeksowania (operator[]
) umożliwiający stosowanie naturalnej składni odwołań do elementów tablicy. Destruktor wykorzystuje operator delete []
aby automatycznie usunąć wskazywaną tablicę.
try
{
std::unique_ptr<Gadget[]> many_gadgets {new Gadget[10]};
for(int i = 0; i < 10; ++i)
{
many_gadgets[i].do_stuff();
unsafe_use(many_gadgets[i]);
}
}
catch (...)
{
std::cout << "Obsługa wyjątku" << std::endl;
}
Wskaźniki ze zliczaniem odniesień¶
Inteligentne wskaźniki ze zliczaniem odniesień eliminują konieczność kodowania skomplikowanej logiki sterującej czasem życia obiektów współużytkowanych przez pewną liczbę innych obiektów.
Można podzielić je na:
- ingerencyjne (intrusive) – wymagają od klas obiektów zarządzanych udostępnienia specjalnych metod lub składowych, za pomocą których realizowane jest zliczanie odwołań
- nieingerencyjne (non-intrusive) – nie wymagają niczego od obiektu zarządzanego
Ważne
Wskaźniki ze zliczaniem odniesień mogą być przechowywane w kontenerach standardowych (np. vector
, list
, itp.)
Klasa std::weak_ptr
¶
Plik nagłówkowy: <memory>
Najbardziej znanym problemem, związanym ze wskaźnikami opartymi na zliczaniu odniesień, są odniesienia cykliczne.
Występują one w sytuacji, gdy kilka obiektów trzyma wskaźniki do siebie nawzajem, przez co licznik odniesień nie spada do zera i wskazywane obiekty nie są nigdy kasowane.
Rozwiązaniem tego problemu jest zastosowanie słabych wskaźników – obiektów std::weak_ptr
.
Wskaźnik typu std::weak_ptr
obserwuje wskazywany obiekt, ale nie ma kontroli nad czasem jego życia i nie może zmieniać jego licznika odniesień.
Nie udostępnia operatorów dereferencji. Aby mieć dostęp do wskazywanego obiektu konieczne jest dokonanie konwersji do std::shared_ptr
.
Gdy wskazywany obiekt został już skasowany konwersja na std::shared_ptr
daje w wyniku:
- wskaźnik pusty - w przypadku metody
lock()
- zgłasza wyjątek
std::bad_weak_ptr
- w przypadku konstruktorashared_ptr<T>(const weak_ptr<T>&)
Przechowywyanie std::weak_ptr
w kontenerach asocjacyjnych¶
Aby przechować wskaźniki std::weak_ptr
w kontenerach asocjacyjnych należy klasy std::owner_less
jako parameteru definiującego sposób porównania
wskaźników.
Klasa std::owner_less
dostarcza implementację umożiwiającą porównanie wskaźników std::shared_ptr
oraz std::weak_ptr
na podstawie prawa własności, a nie wartości
przechowywanych wskaźników. Dwa wskaźniki są uznane za równoważne tylko wtedy, gdy oba są puste lub oba zarządzają tym samym obiektem (współdzielą blok kontrolny).
std::map<std::shared_ptr<Key>, Value, std::owner_less<std::shared_ptr<Key>>> my_map;
std::set<std::weak_ptr<Key>, std::owner_less<std::weak_ptr<Key>>> my_set;
Zastosowanie std::weak_ptr
¶
Wskaźniki std::weak_ptr
stosuje się do:
- zrywania cyklicznych zależności dla
shared_ptr
- współużytkowania zasobu bez przejmowania odpowiedzialności za zarządzanie nim
- eliminowania ryzykownych operacji na wiszących wskaźnikach