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::shared_ptr

Plik nagłówkowy: <memory>

std::shared_ptr jest szablonem nieingerencyjnego wskaźnika zliczającego odniesienia do wskazywanych obiektów.

Działanie:

  • konstruktor tworzy licznik odniesień i inicjuje go wartością 1
  • konstruktor kopiujący lub operator przypisania inkrementują licznik odniesień
  • destruktor zmniejsza licznik odniesień, jeżeli ma on wartość 0, to usuwa obiekt wywołując domyślnie operator delete

Przykład 1

#include <memory>

using namespace std;

class Gadget { /* implementacja */ };

map<string, shared_ptr<Gadget>> gadgets;

void foo()
{
    shared_ptr<Gadget> p1 {new Gadget(1)};  // reference counter = 1
    {
        shared_ptr<Gadget> p2 = p1; // copying of shared_ptr
        // reference counter == 2

        gadgets.insert(make_pair("mp3 player", p2)); // copying shared_ptr to a std container
        // reference counter == 3

        p2->use();
    }  // destruction of p2 decrements reference counter = 2
}  // destruction of p1 decrements reference counter = 1

// ...

gadgets.clear(); // reference counter = 0 - gadget is removed

Przydatne metody z interfejsu shared_ptr<T>

T* get() const

zwraca przechowywany wskaźnik.

void reset()

zwalnia prawo własności do zarządzanego obiektu.

template <typename Y*> void reset(Y* ptr)

zamienia obiekt zarządzany na obiekt wskazywany przez ptr.

bool unique() const

zwraca true, jeśli obiekt shared_ptr, na rzecz którego nastąpiło wywołanie, jest jedynym właścicielem przechowywanego wskaźnika. W pozostałych przypadkach zwraca false

long use_count() const

zwraca wartość licznika odwołań do wskaźnika przechowywanego w obiekcie shared_ptr. Przydatna w diagnostyce.

void swap(shared_ptr<T>& other)

wymiana wskaźników między dwoma obiektami shared_ptr. Wymienia wskaźniki oraz liczniki odwołań.

explicit operator bool() const

umożliwia konwersję do wartości logicznej, np. if (p && p->is_valid())

Fabryka make_shared

Używanie shared_ptr eliminuje konieczność stosowanie operatora delete, jednakże nie eliminuje użycia new. Można uniknąć używania operatora new stosując zamiast tego funkcję pomocniczą make_shared(), która pełni rolę fabryki wskaźników shared_ptr. Funkcja przekazuje swoje parametry do konstruktora obiektu kontrolowanego przez inteligentny wskaźnik (perfect forwarding).

std::shared_ptr<std::string> x = std::make_shared<std::string>("hello, world!");
std::cout << *x << std::endl;

Stosowanie funkcji make_shared() jest wydajniejsze niż konstrukcja shared_ptr(new std::string) ponieważ alokowany jest tylko jeden segment pamięci, w którym umieszczany jest wskazywany obiekt oraz blok kontrolny z licznikami odniesień.

Problem tymczasowych obiektów typu shared_ptr

Ważne

Zawsze należy używać nazwanych instancji shared_ptr!

Użycie tymczasowych zmiennych anonimowych typu shared_ptr może powodować wycieki pamięci.

Przykład:

void f(shared_ptr<int>, int);
int may_throw();

void ok()
{
    shared_ptr<int> p { new int(2) };
    f(p, may_throw());
}

void bad()
{
    f(shared_ptr<int> {new int(2)}, may_throw() );
}

Ponieważ kolejność ewaluacji argumentów funkcji nie jest określona, jest możliwa sytuacja w której wykona się new, następnie funkcja may_throw rzuci wyjątkiem. Objekt``shared_ptr`` nie został w ogóle skonstruowany, nie wykona się jego destruktor, a tym samym zasób nie zostanie uprzątnięty.

Powyższy problem może zostać również rozwiązany przy użyciu funkcji make_shared().

Dealokatory

Niekiedy pojawia się potrzeba zastosowania wskaźnika shared_ptr do kontroli zasobu takiego typu, że jego zwolnienie nie sprowadza się do prostego wywołania operatora delete. Takie przypadki shared_ptr obsługuje przy pomocy dealokatorów użytkownika.

Dealokator jest:

  • obiektem funkcyjnym odpowiedzialnym za zwolnienie zasobu, kiedy liczba referencji spadnie do zera.
  • przekazywany jako drugi argument konstruktora shared_ptr.
class SocketCloser
{
public:
    void operator()(Socket* s)
    {
        s->close();
    }
};

void use_device()
{
    Socket socket;
    std::shared_ptr<Socket> safe_socket(&socket, SocketCloser());

    may_throw(); // może wylecieć wyjątek

    safe_socket->write("Data");
} // zasób Socket został bezpiecznie zwolniony za pomocą metody close()

Rzutowania między wskaźnikami shared_ptr

Problemy związane z rzutowaniami między wskaźnikami shared_ptr rozwiązują trzy funkcje szablonowe:

template<class T, class U> shared_ptr<T> static_pointer_cast(shared_ptr<U> const & r)
  • Jeśli r jest pusty, zwraca pusty shared_ptr<T>
  • W innym przypadku shared_ptr<T> przechowuje kopię static_cast<T*>(r.get()) i współdzieli prawo własności z r
template<class T, class U> shared_ptr<T> const_pointer_cast(shared_ptr<U> const &r)
  • Jeśli r jest pusty, zwraca pusty shared_ptr<T>
  • W innym przypadku shared_ptr<T> przechowuje kopię const_cast<T*>(r.get()) i współdzieli prawo własności z r
template<class T, class U> shared_ptr<T> dynamic_pointer_cast(shared_ptr<U> const & r)
  • Jeśli dynamic_cast<T*>(r.get()) zwraca wartość różną od nullptr, zwracany jest obiekt shared_ptr<T>, który współdzieli prawo własności do r
  • W przeciwnym wypadku pusty shared_ptr<T>

Zależności cykliczne między wskaźnikami shared_ptr

Problemem dla wskaźników shared_ptr są zależności cykliczne, które powodują wycieki zasobów (pamięci).

struct Cyclic
{
    shared_ptr<Cyclic> me_;
    /* … */
};

void foo()
{
    shared_ptr<Cyclic> cptr = make_shared<Cyclic>();
    cptr->me_ = cptr;
}  // wyciek pamięci

Ważne

Cykle muszą być przerywane za pomocą wskaźników std::weak_ptr

Tworzenie wskaźnika shared_ptr ze wskaźnika this

Niekiedy istnieje konieczność utworzenia inteligentnego wskaźnika shared_ptr ze wskaźnika this. Oznacza to, że obiekt danej klasy będzie zarządzany za pośrednictwem inteligentnego wskaźnika. W ogólności rozwiązaniem problemu konwersji this do shared_ptr jest użycie wskaźnika typu std::weak_ptr, który jest obserwatorem wskaźników shared_ptr (pozwala na podglądanie wskazywanego obiektu bez ingerowania w wartość licznika odwołań). Zdefiniowanie w klasie składowej typu weak_ptr i zainicjowanie jej wartością this umożliwia późniejsze pozyskiwanie wskaźników shared_ptr. Aby nie trzeba było za każdym razem pisać tego samego kodu, można wykorzystać dziedziczenie po pomocniczej klasie enable_shared_from_this.

Klasa enable_shared_from_this<T> umożliwia obiektowi typu T, który jest zarządzany przez instancję std::shared_ptr<T> bezpieczne tworzenie dodatkowych wskaźników typu std::shared_ptr<T>, które współdzielą prawo własności do zarządzanego obiektu.

Przykład nieprawidłowego generowania instancji shared_ptr ze wskaźnika this:

#include <memory>

void do_stuff(std::shared_ptr<A> p)
{
   // using p
}

// ...

class A
{
public:
   void call_do_stuff()
   {
      do_stuff(std::shared_ptr<A>(this));
   }
};

int main()
{
   auto p = std::make_shared<A>();
   p->call_do_stuff();
}

Prawdiłowe tworzenie instancji std::shared_ptr ze wskaźnika this:

#include <memory>

void do_stuff(std::shared_ptr<A> p)
{
   // using p
}

// ...

class A : public std::enable_shared_from_this<A>
{
public:
   void call_do_stuff()
   {
      do_stuff(shared_from_this());
   }
};

int main()
{
   auto p = std::make_shared<A>();
   p->call_do_stuff();
}

std::shared_ptr – podsumowanie

Wskaźniki std::shared_ptr można skutecznie stosować tam, gdzie:

  • jest wielu użytkowników obiektu, ale nie ma jednego jawnego właściciela
  • trzeba przechowywać wskaźniki w kontenerach biblioteki standardowej
  • trzeba przekazywać wskaźniki do i z bibliotek, a nie ma jawnego wyrażenia transferu własności

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 konstruktora shared_ptr<T>(const weak_ptr<T>&)

Konwersja weak_ptr na shared_ptr

Przykład 1

weak_ptr<Gadget> weakPtr;
shared_ptr<Gadget> ptr(new Gadget());

weakPtr = ptr; // ref counter == 1

ptr.reset();  // ref counter == 0 -> gadget is destroyed

shared_ptr<Gadget> tempPtr = weakPtr.lock();

if (tempPtr)
{
    tempPtr->do_stuff();
}

Przykład 2

weak_ptr<Gadget> weakPtr;
shared_ptr<Gadget> ptr = make_shared<Gadget>();

weakPtr = ptr; // ref counter == 1

ptr.reset();  // gadget is destroyed

try
{
    shared_ptr<Gadget> anotherPtr(weakPtr);
}
catch(const bad_weak_ptr& e)
{
    cout << e.what() << endl;
}

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