Inteligentne wskaźniki - Smart Pointers¶
Mechanizm RAII¶
Mechanizm wyjątków a zasoby¶
Jawne i ręczne zarządzanie zasobami jest podatne na wycieki:
void send(Msg* msg, std::string_view destination)
{
auto port = open_port(destination);
my_mutex.lock();
send(port, msg); // if throws - port, mutex & msg will leak
my_mutex.unlock();
close_port(port);
delete msg;
}
Używanie konstrukcji try-catch
w celu unikania wycieków jest trudne, mało czytelne i prowadzi do duplikacji kodu
// Nieudolna poprawa
void send(Msg* msg, std::string_view destination)
{
auto port = open_port(destination);
my_mutex.lock();
try
{
send(port, msg); // if throws - port, mutex & msg leak
}
catch(...)
{
my_mutex.unlock();
close_port(port);
delete msg;
throw;
}
my_mutex.unlock();
close_port(port);
delete msg;
}
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ą (destrukcją).
Pozyskanie zasobu jest połączone z konstrukcją, a zwolnienie zasobu z automatyczną destrukcją lokalnej zmiennej. Ponieważ zagwarantowane jest wywołanie destruktora zmiennej lokalnej w momencie wyjścia z zakresu, zasób zostanie zwolniony od razu gdy skończy się czas życia kontrolującego obiektu. Mechanizm ten działa również przy wystąpieniu wyjątku. RAII jest kluczową koncepcją przy pisaniu kodu odpornego na wycieki zasobów.
Poprawny kod stosujący RAII wygląda następująco:
void send(std::unique_ptr<Msg> msg, std::string_view destination) // msg owns the Msg
{
Port port{open_port(destination)}; // port owns the PortHandle
std::lock_guard lk{my_mutex}; // lk owns the lock
send(port, msg.get());
} // automatically unlocks my_mutex, closes the port & deletes the msg
W powyższej implementacji klasy zarządzające zasobami to std::unique_ptr
, std::lock_guard
oraz Port
:
Implementacja std::lock_guard
:
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;
}
Implementacja klasy Port
:
class Port {
PortHandle port_;
public:
Port(std::string_view destination) : port_{open_port(destination)} { }
~Port() { close_port(port_); }
operator PortHandle() { return port_; }
Port(const Port&) = delete;
Port& operator=(const Port&) = delete;
};
Obie klasy wykorzystują symetrię operacji konstruktor/destruktor i wykonują odpowiednio operacje:
lock()
/unlock()
open_port()
/close_port()
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 jednoczesnym 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żliwiają 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 |
---|---|---|---|
|
o |
o |
|
|
o |
||
|
o |
o |
wewnętrzny |
|
|||
|
o |
o |
wewnętrzny |
|
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 lvalue 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)
{
return std::unique_ptr<Gadget> {new Gadget(arg)};
}
auto ptr_gadget = create_gadget(1); // implicit transfer of ownership
ptr_gadget->do_something();
std::unique_ptr<Gadget> another_ptr = std::move(ptr_gadget); // explicit transfer of ownership
Od C++14 funkcję create_gadget()
można zastąpić biblioteczną funkcją std::make_unique()
, która transferuje swoje argumenty wywołania do konstruktora alokowanego na stercie obiektu:
auto ptr_gadget = std::make_unique<Gadget>(arg); // C++14
może być przekazywany (przenoszony) do funkcji jako parametr sink
void sink(unique_ptr<Gadget> gadget)
{
gadget->call_method();
// sink takes ownership of gadget - deletes the object pointed by gadget
}
sink(std::move(ptr_gadget)); // explicitly moving into sink
sink(create_gadget(2)); // implicit move
sink(std::make_unique<Gadget>(42)); // implicit move
może być przechowywany w kontenerach STL
auto gadget = std::make_unique<Gadget>(67);
std::vector<std::unique_ptr<Gadget>> gadgets;
gadgets.push_back(std::move(gadget)); // explicit transfer of ownership
gadgets.push_back(std::make_unique<Gadget>(31)); // implicit move to 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", "w"), &fclose};
fprintf(file.get(), "Id %d", 42);
} // 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óżny 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ące 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()
Specjalizacja dla tablic 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ę.
Specjalizacja std::unique_ptr<T[]>
wykorzystywana jest głównie do bezpiecznej obsługi kodu legacy
namespace Legacy
{
int* create_array(size_t size)
{
return new int[size]; // creates dynamic array on heap
}
}
//...
{
int size = 1024;
std::unique_ptr<int[]> data{create_array(size)};
for(int i = 0; i < size; ++i)
{
data[i] = i; // operator[]
may_throw(data);
} // automatic delete[] on allocated array
}
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<T>¶
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 konstruktorastd::shared_ptr<T>(const std::weak_ptr<T>&)
Przechowywanie std::weak_ptr w kontenerach asocjacyjnych¶
Aby przechować wskaźniki std::weak_ptr
w kontenerach asocjacyjnych należy użyć klasy std::owner_less
jako parametru definiującego sposób porównania
wskaźników.
Klasa std::owner_less
dostarcza implementację umożliwiają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
std::shared_ptr
współużytkowania zasobu bez przejmowania odpowiedzialności za zarządzanie nim
eliminowania ryzykownych operacji na wiszących wskaźnikach