Mamy gwarancję, że zarządzane dane istnieją tak długo, jak długo
przynajmniej jeden wskaźnik inteligentny shared_ptr
nimi
zarządzadza. Tej gwarancji jednak nie zawsze potrzebujemy. Czasami
wystarczy nam możliwości sprawdzenia czy zarządzane dane istnieją i,
jeżeli trzeba, bezpiecznego użycia ich. Można powiedzieć, że
wystarczy nam śledzenie danych bez posiadania własności, czyli nie
wymagamy, aby dane istniały.
W C++ tę funkcjonalność dostarcza słaby inteligentny wskaźnik,
który jest zaimplementowany przez szablon klasy std::weak_ptr
.
Funkcjonalność słabego wskaźnika jest powiązana ze wskaźnikiem typu
share_ptr
, ponieważ słaby wskaźnik w pewien niepełny sposób
współdzieli zarządzane dane. Poniższy przykład ilustruje i motywuje
potrzebę słabego wskaźnika.
Implementujemy fabrykę zwracającą dane, które mogą być bardzo duże. Fabryka powinna:
stworzyć nowe dane, jeżeli nie istnieją,
użyć wcześniej stworzonych danych, jeżeli jeszcze istnieją.
Fabryka powinna śledzić stworzone dane (bez posiadania ich) i ewentualnie ich użyć. Istnienie danych zależy od sposobu ich użycia przez kod wywołujący fabrykę, czyli od tego, kiedy współdzielone wskaźniki są niszczone.
Fabrykę najlepiej zaimplementować z użyciem słabych wskaźników. Zanim przedstawimy implementację fabryki, pierwsze omówimy podstawy słabych wskaźników.
std::weak_ptr
#include <memory>
Od C++11
Szablon klasy, którego argumentem jest typ śledzonych danych.
Słaby wskaźnik może być kopiowany i przenoszony, ale nie jest to
takie ważne, jak przy unique_ptr
i shared_ptr
.
Słaby wskaźnik jest tworzony od współdzielonego wskaźnika.
Słaby wskaźnik nigdy nie niszczy zarządzanych danych.
Żeby odwołać się do danych słabego wskaźnika, musimy stworzyć współdzielony wskaźnik. Uda się, jeżeli dane istnieją.
Zarządzane dane nie wiedzą, że są zarządzane, czyli typ zarządzanych danych nie musi być przygotowany w jakiś specjalny sposób, na przykład nie musi być wyprowadzony z jakiejś klasy bazowej.
Przykład niżej demonstruje użycie wskaźnika:
#include <cassert>
#include <iostream>
#include <memory>
using namespace std;
struct A
{
A()
{
cout << "ctor\n";
}
void
saysomething()
{
cout << "Hi!\n";
}
~A()
{
cout << "dtor\n";
}
};
int
main()
{
auto sp = make_shared<A>();
weak_ptr<A> wp(sp);
// The assert below doesn't compile, because it would suggest the
// same semantics as for the unique and shared pointers, that we've
// got the managed data, and can use them (which is wrong for the
// weak pointer).
// assert(wp);
// Instead we can use function 'expired' of the weak pointer, which
// should alert us of special semantics.
assert(!wp.expired());
// Here the managed data exist.
{
shared_ptr<A> sp2(wp);
sp2->saysomething();
assert(sp2);
}
// Release the ownership from sp. Since sp was the sole managing
// object, the managed data are destroyed.
sp.reset();
// Here the managed data is gone.
assert(wp.expired());
}
Problem. W jaki sposób bezpiecznie (czyli bez zjawiska hazardu) użyć
dane słabego wskaźnika, jeżeli one istnieją? Nawet jeżeli
sprawdziliśmy, że dane ciągle istnieją (używając funkcji expired
),
to nie możemy użyć surowego wskaźnika, ponieważ może on już być
nieaktualny. Na szczęście, weak_ptr
nie pozwala na dostęp do
surowego wskaźnika, tak jak pozwalają na to unique_ptr
i
shared_ptr
, czyli z użyciem operatora wyłuskania (operator *
),
operatora dostępu do składowej przez wskaźnik (operator ->
) czy
funkcji get
.
Rozwiązanie. Zabezpiecz zarządzane dane (pozyskaj spółdzieloną własność) poprzez stworzenie współdzielonego wskaźnika ze słabego wskaźnika. Możemy to zrobić na dwa sposoby:
wywołać konstruktor shared_ptr
ze słabym wskaźnikiem; konstruktor
rzuci wyjątek, jeżeli dane słabego wskaźnika już nie istnieją,
wywołać funkcję (składową słabego wskaźnika) lock
, która zwróci
współdzielony wskaźnik; współdzielony wskaźnik będzie pusty, jeżeli
zarządzane dane już nie istnieją.
Oto przykład:
#include <cassert>
#include <iostream>
#include <memory>
using namespace std;
int
main()
{
auto sp = make_shared<int>();
weak_ptr<int> wp(sp);
// Here the managed data exist.
{
shared_ptr<int> sp(wp);
assert(sp);
}
// Here the managed data exist.
{
shared_ptr<int> sp = wp.lock();
assert(sp);
}
// Flush the managed data.
sp.reset();
// Here the managed data is gone.
try
{
shared_ptr<int> sp(wp);
}
catch (std::bad_weak_ptr &)
{
cout << "Caught a std::bad_weak_ptr.\n";
}
// Here the managed data is long gone.
{
shared_ptr<int> sp = wp.lock();
assert(!sp);
}
}
Struktura sterująca grupy współdzielonych wskaźników jest także używana przez słabe wskaźniki, które także należą do grupy, ale bez posiadania własności.
Struktura sterująca posiada nie tylko licznik odwołań (który przechowuje liczbę współdzielonych wskaźników), ale także licznik słabych odwołań, który przechowuje liczbę słabych wskaźników.
Wiemy, że dane zarządzane są niszczone, kiedy licznik odwołań osiągnie zero. Z kolei struktura sterująca jest niszczona, kiedy licznik odwołań i licznik słabych odwołań osiągną zero.
Oto implementacja:
#ifndef FACTORY_HPP
#define FACTORY_HPP
#include <map>
#include <memory>
template <typename V, typename K>
auto
factory(const K &k)
{
static std::map<K, std::weak_ptr<V>> cache;
std::shared_ptr<V> sp;
auto i = cache.find(k);
if (i != cache.end())
sp = i->second.lock();
if (sp == nullptr)
{
sp = std::make_shared<V>(k);
cache.insert(i, make_pair(k, sp));
}
return sp;
}
#endif // FACTORY_HPP
#include "factory.hpp"
#include <cassert>
#include <iostream>
#include <map>
#include <memory>
using namespace std;
struct A
{
static int counter;
int m_id;
int m_unique;
A(int id): m_id(id), m_unique(counter++)
{
cout << "ctor: " << m_id << ", " << m_unique << '\n';
}
~A()
{
cout << "dtor: " << m_id << ", " << m_unique << '\n';
}
};
int A::counter = 0;
int
main()
{
int unique;
{
auto sp1 = factory<A>(1);
auto sp2 = factory<A>(1);
unique = sp1->m_unique;
assert(sp1->m_unique == sp2->m_unique);
}
auto sp1 = factory<A>(1);
assert(unique != sp1->m_unique);
}
Słaby wskaźnik zabiera dwa razy więcej pamięci, niż surowy wskaźnik, ponieważ słaby wskaźnik posiada jako pola składowe:
surowy wskaźnik do zarządzanych danych,
surowy wskaźnik do struktury sterującej.
Dlaczego przechowywany jest surowy wskaźnik do zarządzanych danych, skoro i tak nie mamy do niego dostępu? Ponieważ jest potrzebny przy tworzeniu współdzielonego wskaźnika.
Wiemy, że z powodu wydajności współdzielonych wskaźników, surowy wskaźnik do zarządzanych danych nie może być częścią struktury sterującej. Tak więc słaby wskaźnik musi przechowywać ten surowy wskaźnik jako swoje pole składowe.
Słaby wskaźnik śledzi zarządzane dane, ale ich nie posiada.
Słaby wskaźnik zawsze tworzymy ze współdzielonego wskaźnika.
Możemy stworzyć współdzielony wskaźnik ze słabego wskaźnika, jeżeli zarządzane dane ciągle istnieją.
Słaby wskaźnik nigdy nie niszczy danych.
Dlaczego potrzebujemy weak_ptr
?
Jaka jest różnica między shared_ptr
i weak_ptr
?
Czy możemy stworzyć wskaźnik weak_ptr
na podstawie wskaźnika
unique_ptr
?