Inteligentne wskaźniki#
Zarządzanie zasobami w C++ - RAII#
Mechanizm wyjątków a zasoby#
Jawne zarządzanie zasobami w C++ jest trudne, ponieważ wymaga ręcznego zarządzania zasobami. W przypadku wystąpienia wyjątku, zasoby mogą nie zostać zwolnione, co prowadzi do wycieków zasobów.
void leaky_send(Data* data, const std::string& destination)
{
auto port = open_port(destination);
some_mutex.lock();
send(port, data); // it may throw
may_throw(); // it may throw
some_mutex.unlock(); // leaks if send() throws
close_port(port); // leaks if send() throws
delete data; // leaks if send() throws
}
RAII - Resource Acquisition Is Initialization#
Technika łączy przejęcie i zwolnienie zasobu z inicjalizacją zmiennych lokalnych i ich automatyczną destrukcją.
Pozyskanie zasobu jest połączone z konstrukcją, a zwolnienie z automatyczną destrukcją obiektu.
Ponieważ wywołanie destruktora dla obiektu lokalnego następuje automatycznie, 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.
void safe_send(std::unique_ptr<Data> data, const std::string& destination)
{
Port port{destination};
std::lock_guard<std::mutex> lk{some_mutex};
// ...
send(port, data); // it may throw
may_throw(); // it may throw
} // automatically unlocks some_mutex, closes the port and deletes the pointer in data
Klasy RAII#
Klasa RAII to klasa, która pozyskuje zasób w konstruktorze a w destruktorze go zwalnia:
class Port {
PortHandle port_;
public:
Port(const std::string& destination) : port_{open_port(destination)} {}
~Port() { close_port(port_); }
operator PortHandle() { return port_; }
// port handles can't usually be cloned
// so disable copying and assignment if necessary
Port(const Port&) = delete;
Port& operator=(const Port&) = delete;
};
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;
};
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 |
std::unique_ptr#
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 lvalue jawnego transferu przy pomocy funkcji std::move().
#include <memory>
void f()
{
std::unique_ptr<Gadget> ptr_gadget(new Gadget{1, "ipod"});
ptr_gadget->use();
may_throw();
std::unique_ptr<Gadget> other_ptr_gadget = std::move(ptr_gadget);
other_ptr_gadget->use();
} // allocated Gadget is automatically deleted
Od C++14 dostępna jest funkcja std::make_unique() ułatwiająca tworzenie obiektów z wykorzystaniem std::unique_ptr.
auto ptr_gadget = std::make_unique<Gadget>(1, "ipod");
Semantyka przenoszenia dla std::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()
{
static int id_gen = 0;
const int id = ++id_gen;
auto ptr_gadget = std::make_unique<Gadget>(id, "Gadget#" + std::to_string(id));
assert(ptr_gadget->is_valid());
return ptr_gadget;
}
auto gadget = create_gadget();
if (gadget)
gadget->use();
może być przekazywany przez wartość (przenoszony) do funkcji jako parametr sink
void sink(unique_ptr<Gadget> gadget)
{
if (gadget)
gadget->use();
} // sink takes ownership - deletes the object pointed by gadget
auto gadget = create_gadget();
sink(move(gadget)); // explicitly moving into sink
sink(create_gadget()); // implicit move
może być przechowywany w kontenerach STL (w standardzie C++11)
std::vector<std::unique_ptr<Gadget>> gadgets;
gadgets.push_back(std::make_unique<Gadget>(42, "smartwatch")); // implicit move to the container
gadgets.push_back(create_gadget()); // implicit move to the container
auto gadget = std::make_unique<Gadget>(665, "smartphone");
gadgets.push_back(std::move(gadget)); // explicit move to the container
for(const auto& g : gadgets)
g->use();
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* g = new SuperGadget();
g->do_something();
Odpowiednik z użyciem unique_ptr
std::unique_ptr<Gadget> g = std::make_unique<SuperGadget>();
g->do_something();
//explicit conversion - hard to miss it
auto another_g = 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:
void f()
{
std::unique_ptr<FILE, int(*)(FILE*)> file{fopen("test.txt"), &fclose};
std::vector<char> buffer(1024);
read_file_to_buffer(file.get(), vec.data(), vec.size());
// rest of the code
} // fclose() is called for an opened file
Important
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ę przed klientem
Plik bitmap.hpp:
class Bitmap
{
public:
// rest of the interface...
~Bitmap(); // must be only declared
private:
class Impl; // forward declaration
std::unique_ptr<Impl> pimpl_; // pointer to implementation
};
Plik bitmap.cpp:
#include "bitmap.hpp"
class Bitmap::Impl
{
std::vector<Pixel> pixels_;
int width_;
int height_;
};
Bitmap::Bitmap() : pimpl_ {std::make_unique<Impl>()}
{
// implementation details...
}
Bitmap::~Bitmap() = default; // important! after defintion of Bitmap::Impl()
Specjalizacja std::unique_ptr<T[]> dla tablic#
Klasa std::unique_ptr<T[]> jest specjalizacją szablonu std::unique_ptr dla tablic obiektów typu T.
Klasa ta stanowi lepszą alternatywę dla klasycznych tablic przydzielanych dynamicznie.
Zgodnie z zasadą RAII, klasa
std::unique_ptr<T[]>automatycznie zwalnia pamięć po tablicy przy wyjściu z zakresu.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ę.
Important
Klasa std::unique_ptr<T[]> jest użyteczna w przypadku gdy wywołujemy kod legacy korzystający z tablic alokowanych dynamicznie.
Gdy chcemy użyć tablicy w nowym kodzie, zalecane jest użycie kontenerów STL, takich jak std::vector<T>.
namespace LegacyCode
{
int* create_buffer(size_t size)
{
return new int[size];
}
}
void use_legacy_buffers()
{
const size_t size = 1024;
std::unique_ptr<int[]> buffer{create_buffer(size)};
for(size_t i = 0; i < size; ++i)
buffer[i] = i;
may_throw();
// rest of the code
} // buffer is automatically deleted
std::weak_ptr#
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 wynik:
wskaźnik pusty - w przypadku metody
lock()shared_ptr<Gadget> ptr_g = make_shared<Gadget>(1, "ipad"); std::weak_ptr<Gadget> weak_g = ptr_g; assert(ptr_g.use_count() == 1); // reference counter = 1 ptr_g.reset(); // reference counter = 0, gadget is deleted std::shared_ptr<Gadget> temp_g = weak_g.lock(); if (temp_g) { temp_g->use(); }
zgłasza wyjątek
std::bad_weak_ptr- w przypadku konstruktorastd::shared_ptr<T>(const std::weak_ptr<T>&)std::shared_ptr<Gadget> ptr_g = make_shared<Gadget>(1, "ipad"); std::weak_ptr<Gadget> weak_g; weak_g_ = ptr_g; // reference counter = 1 ptr_g.reset(); // gadget is destroyed try { std::shared_ptr<Gadget> temp_g(weak_g); // throws std::bad_weak_ptr } catch(const std::bad_weak_ptr& e) { std::cout << e.what() << std::endl; }
Przechowywanie 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ż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).
struct Key { /* implementation */ };
struct Value { /* implementation */ };
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_ptrwspółużytkowania zasobu bez przejmowania odpowiedzialności za zarządzanie nim
eliminowania ryzykownych operacji na wiszących wskaźnikach