W przeciwieństwie do std::unique_ptr, który jest przeznaczony do pojedynczego posiadania zasobu i zarządzania nim, std::shared_ptr ma na celu rozwiąż przypadek, w którym potrzebujesz wielu inteligentnych wskaźników będących współwłaścicielami zasobu.
Oznacza to, że dobrze jest mieć wiele std::shared_ptr wskazujących na ten sam zasób. Wewnętrznie std::shared_ptr śledzi, ile std::shared_ptr udostępnia zasób. Dopóki co najmniej jeden std::shared_ptr wskazuje na zasób, zasób nie zostanie zwolniony, nawet jeśli poszczególne std::shared_ptr zostaną zniszczone. Gdy tylko ostatni std::shared_ptr zarządzający zasobem wyjdzie poza zakres (lub zostanie ponownie przypisany tak, aby wskazywał coś innego), zasób zostanie zwolniony.
Podobnie jak std::unique_ptr, std::shared_ptr znajduje się w nagłówku <memory>.
#include <iostream>
#include <memory> // for std::shared_ptr
class Resource
{
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
};
int main()
{
// allocate a Resource object and have it owned by std::shared_ptr
Resource* res { new Resource };
std::shared_ptr<Resource> ptr1{ res };
{
std::shared_ptr<Resource> ptr2 { ptr1 }; // make another std::shared_ptr pointing to the same thing
std::cout << "Killing one shared pointer\n";
} // ptr2 goes out of scope here, but nothing happens
std::cout << "Killing another shared pointer\n";
return 0;
} // ptr1 goes out of scope here, and the allocated Resource is destroyedWypisuje:
Resource acquired Killing one shared pointer Killing another shared pointer Resource destroyed
W powyższym kodzie tworzymy dynamiczny obiekt Resource i ustawiamy std::shared_ptr o nazwie ptr1 zarządzać tym. Wewnątrz zagnieżdżonego bloku używamy konstruktora kopiującego, aby utworzyć drugi std::shared_ptr (ptr2), który wskazuje na ten sam zasób. Kiedy ptr2 wyjdzie poza zakres, zasób nie zostanie zwolniony, ponieważ ptr1 nadal wskazuje na zasób. Kiedy ptr1 wykracza poza zakres, ptr1 zauważa, że nie ma już std::shared_ptr zarządzających zasobem, więc zwalnia zasób.
Zauważ, że utworzyliśmy drugi wspólny wskaźnik z pierwszego udostępnionego wskaźnika. To jest ważne. Rozważmy następujący podobny program:
#include <iostream>
#include <memory> // for std::shared_ptr
class Resource
{
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
};
int main()
{
Resource* res { new Resource };
std::shared_ptr<Resource> ptr1 { res };
{
std::shared_ptr<Resource> ptr2 { res }; // create ptr2 directly from res (instead of ptr1)
std::cout << "Killing one shared pointer\n";
} // ptr2 goes out of scope here, and the allocated Resource is destroyed
std::cout << "Killing another shared pointer\n";
return 0;
} // ptr1 goes out of scope here, and the allocated Resource is destroyed againTen program wypisuje:
Resource acquired Killing one shared pointer Resource destroyed Killing another shared pointer Resource destroyed
a następnie ulega awarii (przynajmniej na komputerze autora).
Różnica polega na tym, że utworzyliśmy dwa std::shared_ptr niezależnie od siebie. W rezultacie, mimo że oba wskazują na ten sam Zasób, nie są sobie wzajemnie świadome. Kiedy ptr2 wykracza poza zakres, myśli, że jest jedynym właścicielem zasobu i zwalnia go. Kiedy ptr1 później wyjdzie poza zakres, myśli tak samo i ponownie próbuje usunąć zasób. Potem dzieją się złe rzeczy.
Na szczęście można tego łatwo uniknąć: jeśli potrzebujesz więcej niż jednego std::shared_ptr do danego zasobu, skopiuj istniejące std::shared_ptr.
Najlepsza praktyka
Zawsze twórz kopię istniejącego std::shared_ptr, jeśli potrzebujesz więcej niż jednego std::shared_ptr wskazującego na ten sam zasób.
Podobnie jak z std::unique_ptr, std::shared_ptr może być wskaźnikiem zerowym, więc przed użyciem sprawdź, czy jest prawidłowy.
std::make_shared
Podobnie jak std::make_unique() może być użyta do utworzenia std::unique_ptr w C++ 14, std::make_shared() może (i powinna) można użyć do utworzenia std::shared_ptr. std::make_shared() jest dostępna w C++ 11.
Oto nasz oryginalny przykład użycia std::make_shared():
#include <iostream>
#include <memory> // for std::shared_ptr
class Resource
{
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
};
int main()
{
// allocate a Resource object and have it owned by std::shared_ptr
auto ptr1 { std::make_shared<Resource>() };
{
auto ptr2 { ptr1 }; // create ptr2 using copy of ptr1
std::cout << "Killing one shared pointer\n";
} // ptr2 goes out of scope here, but nothing happens
std::cout << "Killing another shared pointer\n";
return 0;
} // ptr1 goes out of scope here, and the allocated Resource is destroyedPowody użycia std::make_shared() są takie same jak std::make_unique() -- std::make_shared() jest prostsze i bezpieczniejsze (nie ma możliwości utworzenia dwóch niezależnych std::shared_ptr wskazujący na ten sam zasób, ale nieświadomi siebie nawzajem przy użyciu tej metody). Jednak std::make_shared() jest również bardziej wydajna niż jej nieużywanie. Przyczyną tego jest sposób, w jaki std::shared_ptr śledzi, ile wskaźników wskazuje na dany zasób.
Zagłębianie się w std::shared_ptr
W przeciwieństwie do std::unique_ptr, który wewnętrznie używa jednego wskaźnika, std::shared_ptr używa wewnętrznie dwóch wskaźników. Jeden wskaźnik wskazuje zarządzany zasób. Drugi wskazuje na „blok kontrolny”, który jest dynamicznie przydzielanym obiektem śledzącym wiele rzeczy, w tym liczbę std::shared_ptr wskazujących na zasób. Kiedy std::shared_ptr jest tworzony za pomocą konstruktora std::shared_ptr, pamięć dla obiektu zarządzanego (który jest zwykle przekazywany) i bloku sterującego (który tworzy konstruktor) jest przydzielana oddzielnie. Jednakże podczas korzystania z std::make_shared() można to zoptymalizować do pojedynczej alokacji pamięci, co prowadzi do lepszej wydajności.
To wyjaśnia również, dlaczego niezależne utworzenie dwóch std::shared_ptr wskazujących na ten sam zasób wpędza nas w kłopoty. Każdy std::shared_ptr będzie miał jeden wskaźnik wskazujący na zasób. Jednakże każdy std::shared_ptr niezależnie przydzieli swój własny blok kontrolny, co wskaże, że jest to jedyny wskaźnik będący właścicielem tego zasobu. Tak więc, gdy std::shared_ptr wyjdzie poza zakres, zwolni zasób, nie zdając sobie sprawy, że inne std::shared_ptr również próbują zarządzać tym zasobem.
Jednakże, gdy std::shared_ptr zostanie sklonowany przy użyciu przypisania kopii, dane w bloku kontrolnym mogą zostać odpowiednio zaktualizowane, aby wskazać, że istnieją teraz dodatkowe std::shared_ptr współzarządzające zasób.
Wspólne wskaźniki można tworzyć z unikalnych wskaźników
Std::unique_ptr można przekonwertować na std::shared_ptr za pomocą specjalnego konstruktora std::shared_ptr, który akceptuje wartość r std::unique_ptr. Zawartość std::unique_ptr zostanie przeniesiona do std::shared_ptr.
Jednakże std::shared_ptr nie można bezpiecznie przekonwertować na std::unique_ptr. Oznacza to, że jeśli tworzysz funkcję, która będzie zwracać inteligentny wskaźnik, lepiej będzie zwrócić std::unique_ptr i przypisać ją do std::shared_ptr, jeśli i kiedy jest to właściwe.
Niebezpieczeństwa std::shared_ptr
std::shared_ptr wiążą się z niektórymi takimi samymi wyzwaniami jak std::unique_ptr -- jeśli std::shared_ptr nie został prawidłowo usunięty (albo dlatego, że został przydzielony dynamicznie i nigdy nie został usunięty, albo był częścią obiektu, który został przydzielony dynamicznie i nigdy nie został usunięty), wówczas zasób, którym zarządza, również nie zostanie zwolniony. Dzięki std::unique_ptr musisz się martwić tylko o to, czy jeden inteligentny wskaźnik zostanie prawidłowo usunięty. Dzięki std::shared_ptr musisz martwić się o nie wszystkie. Jeśli którykolwiek z std::shared_ptr zarządzający zasobem nie zostanie prawidłowo zniszczony, zasób nie zostanie poprawnie zwolniony.
std::shared_ptr i arrays
W C++ 17 i wcześniejszych std::shared_ptr nie zapewnia odpowiedniej obsługi zarządzania tablicami i nie należy go używać do zarządzania tablicą w stylu C. Począwszy od C++ 20, std::shared_ptr obsługuje tablice.
Wnioski
std::shared_ptr zaprojektowano w przypadku, gdy potrzebujesz wielu inteligentnych wskaźników współzarządzających tym samym zasobem. Zasób zostanie zwolniony, gdy ostatnie std::shared_ptr zarządzające zasobem zostanie zniszczone.

