cpp

Wprowadzenie

Kiedy dynamicznie tworzymy dane (albo jakikolwiek inne zasoby) i używamy ich w innych wątkach albo częściach kodu, to pojawia się problem kiedy zniszczyć dane. Jeżeli:

Dlatego powinniśmy niszczyć dane we właściwym momencie, czyli wtedy, kiedy nie są już potrzebne. Niestety, ten właściwy moment jest trudny do określenia, ponieważ może on zależeć od:

Rozwiązaniem problemu jest semantyka współdzielonej własności:

Referencja w Javie i C# ma semantykę współdzielonej własności.

std::shared_ptr

Szczegóły

Użycie

Przykład niżej pokazuje podstawowe użycie.

#include <cassert>
#include <iostream>
#include <memory>
#include <string>
#include <utility>

using namespace std;

struct A
{
  string m_text;

  A(const string &text): m_text(text)
  {
    cout << "ctor: " << m_text << endl;
  }

  ~A()
  {
    cout << "dtor: " << m_text << endl;
  }
};

int main (void)
{
  // sp takes the ownership.
  shared_ptr<A> sp(new A("A1"));
  assert(sp);

  // We make sp manage a new object.  A1 is destroyed.
  sp.reset(new A("A2"));

  {
    // We copy-initialize the ownership.
    shared_ptr<A> sp2(sp);
    assert(sp);
    assert(sp2);

    shared_ptr<A> sp3;
    // We copy-assign the ownership.
    sp3 = sp2;
    assert(sp2);
    assert(sp3);

    // Even though sp2 and sp3 go out of scope, A2 will not be
    // destroyed, because it's still being managed by sp.
  }

  {
    // We move-initialize the ownership.
    shared_ptr<A> sp2(std::move(sp));
    assert(!sp);
    assert(sp2);

    shared_ptr<A> sp3;
    // We move-assign the ownership.
    sp3 = std::move(sp2);
    assert(!sp2);
    assert(sp3);

    // A2 is destroyed, because sp3 (the sole managing object o A2)
    // goes out of scope.
  }

  // We can't release the managed data from being managed, as we are
  // able to do with unique_ptr, because we can't preempt (strip)
  // other shared_ptr objects of their ownership.

  // sp.release();

  // If we want to reset a pointer, we can use the reset function.
  sp.reset();
}

Jak to działa.

Z unique_ptr do shared_ptr

Możemy przenieść własność z obiektu typu unique_ptr do obiektu typu shared_ptr w ten sposób:

#include <memory>

using namespace std;

int
main()
{
  auto up = make_unique<int>();
  shared_ptr<int> sp(up.release());
}

Ale lepiej jest tak:

#include <memory>
#include <utility>

using namespace std;

int
main()
{
  auto up = make_unique<int>();
  shared_ptr<int> sp(std::move(up));
}

Możemy przenieść własność z r-wartości typu unique_ptr do obiektu typu shared_ptr, ponieważ typ shared_ptr ma konstruktor, który przyjmuje przez r-referencję obiekt typu unique_ptr. W przykładzie niżej, tworzymy obiekt typu shared_ptr za podstawie obiektu tymczasowego typu unique_ptr zwracanego przez funkcję:

#include <memory>
#include <utility>

using namespace std;

unique_ptr<int>
factory()
{
  return make_unique<int>();
}

int
main()
{
  shared_ptr<int> sp(factory());
}

Wydajność

Obiekt typu shared_ptr zabiera dwa razy więcej pamięci jak surowy wskaźnik, ponieważ zawiera dwa pola składowe:

Dochodzi do tego jeszcze pamięć dla struktury sterującej, ale to nic wielkiego, bo jest ona współdzielona przez obiekty zarządzające.

Wskaźnik do zarządzanych danych mógłby być przechowywany w strukturze sterującej, ale wtedy dostęp do zarządzanych danych byłby wolniejszy, bo wymagałby dodatkowego odwołania pośredniego (przez wskaźnik).

std::make_shared

Kiedy tworzymy dane zarządzane i obiekt zarządzający, możemy podać typ zarządzanych danych dwa razy (i być może się pomylić):

#include <memory>

using namespace std;

int
main()
{
  // We have to type int twice.
  shared_ptr<int> sp(new int);
  // Bug: constructor and destructor mismatch: int[] vs int
  shared_ptr<int[]> sp2(new int);
  // Bug: constructor and destructor mismatch: int[] vs int[5]
  shared_ptr<int> sp3(new int[5]);
}

Ale możemy użyć szablonu funkcji make_shared i podać typ tylko raz, co jest mniej podatne na błędy:

#include <memory>

using namespace std;

int
main()
{
  // We don't have to write the type twice.
  auto sp = make_shared<int>();
  // We can't mismatch the constructor and destructor.
  auto sp2 = make_shared<int[]>(5);
}

Szablon funkcji make_shared przyjmuje typ zarządzanych danych jako argument szablonu.

Podobnie do funkcji make_unique, funkcja make_shared tworzy dane zarządzane i obiekt zarządzający, a następnie zwraca obiekt zarządzający. Nie będzie narzutu wydajnościowego, ponieważ funkcja najprawdopodobniej będzie wkompilowana, będzie zastosowana optymalizacja wartości powrotu, a konstruktor przenoszący zostanie pominięty.

Co ciekawe, make_shared alokuje w jednym kawałku (czyli z jedną alokacją pamięci) pamięć dla zarządzanych danych i struktury sterującej, a następnie tworzy w miejscu (ang. to create in place, czyli pod wskazanym adresem, bez alokacji pamięci) dane zarządzane i strukturę sterującą, co jest szybsze niż dwie osobne alokacje pamięci.

Podsumowanie

Quiz