cpp

Wprowadzenie

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.

Motywacja

Implementujemy fabrykę zwracającą dane, które mogą być bardzo duże. Fabryka powinna:

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.

Szczegóły

std::weak_ptr

Użycie

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());
}

Tworzenie współdzielonego wskaźnika ze słabego wskaźnika

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:

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);
  }
}

Jak to działa

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.

Implementacja przykładu motywującego

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);
}

Wydajność

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:

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.

Podsumowanie

Quiz