Jakiego wskaźnika używam, kiedy?

228

Ok, więc ostatnim razem, gdy pisałem C ++ na życie, std::auto_ptrbyło wszystko, co było dostępne w standardowej wersji lib, i to boost::shared_ptrbyła wściekłość. Nigdy tak naprawdę nie przyglądałem się innym dostępnym rodzajom inteligentnych wskaźników. Rozumiem, że C ++ 11 zapewnia teraz niektóre typy ulepszeń, ale nie wszystkie.

Czy ktoś ma prosty algorytm, aby określić, kiedy użyć którego inteligentnego wskaźnika? Najlepiej zawierające porady dotyczące głupich wskaźników (surowe wskaźniki, takie jak T*) i resztę inteligentnych wskaźników zwiększających. (Coś jak to byłoby świetnie).

sbi
źródło
Zobacz także std :: auto_ptr na std :: unique_ptr
Martin York
1
Naprawdę mam nadzieję, że ktoś wymyśli fajny przydatny schemat blokowy, taki jak schemat wyboru STL .
Alok Uratuj
1
@Als: Och, to naprawdę fajne! Często zadawałem pytania.
sbi
6
@Deduplicator To nie jest nawet bliskie byciu duplikatem. Połączony pytanie mówi: „Kiedy należy używać do inteligentnego wskaźnika” i to pytanie jest „Kiedy mogę wykorzystać te inteligentne kursory?” tzn. ten kategoryzuje różne zastosowania standardowych inteligentnych wskaźników. Połączone pytanie tego nie robi. Różnica jest na pozór niewielka, ale duża.
Rapptz

Odpowiedzi:

183

Współwłasność: a średnia przyjmowane są prawie takie same jak ich odpowiedniki Google Boost . Używaj ich, gdy chcesz udostępnić zasób i nie wiesz, który z nich będzie ostatnim, który przeżyje. Służy do obserwowania udostępnionego zasobu bez wpływu na jego żywotność, aby nie przerywać cykli. Cykle z nie powinny normalnie się zdarzyć - dwa zasoby nie mogą się posiadać.
shared_ptrweak_ptrweak_ptrshared_ptr

Pamiętaj, że Boost dodatkowo oferuje oferty shared_array, które mogą być odpowiednią alternatywą dla shared_ptr<std::vector<T> const>.

Następnie oferty Boost intrusive_ptr, które są lekkim rozwiązaniem, jeśli Twój zasób oferuje już zarządzanie liczone według referencji i chcesz dostosować je do zasady RAII. Ten nie został przyjęty przez standard.

Unikalna własność:
Boost ma również scoped_ptropcję, której nie można skopiować i dla której nie można określić kasatora. std::unique_ptrjest boost::scoped_ptrna sterydach i powinien być domyślnym wyborem, gdy potrzebujesz inteligentnego wskaźnika . Pozwala określić separator w argumentach szablonu i jest ruchomy , w przeciwieństwie do boost::scoped_ptr. Jest również w pełni użyteczny w kontenerach STL, o ile nie używasz operacji wymagających typów do kopiowania (oczywiście).

Zauważ jeszcze raz, że Boost ma wersję tablicową: scoped_arrayktórą zunifikował standard, wymagając std::unique_ptr<T[]>częściowej specjalizacji, która będzie delete[]wskaźnikiem zamiast deletego (za pomocą default_deleter). std::unique_ptr<T[]>oferuje również operator[]zamiast operator*i operator->.

Pamiętaj, że std::auto_ptrnadal jest w standardzie, ale jest przestarzały . §D.10 [depr.auto.ptr]

Szablon klasy auto_ptrjest przestarzały. [ Uwaga: Szablon klasy unique_ptr(20.7.1) zapewnia lepsze rozwiązanie. —Wskazówka ]

Brak prawa własności:
używaj głupich wskaźników (wskaźników surowych) lub odniesień do nieposiadających własności odniesień do zasobów i gdy wiesz, że zasób przeżyje obiekt / zakres odniesienia. Preferuj odniesienia i używaj surowych wskaźników, gdy potrzebujesz zerowalności lub możliwości resetowania.

Jeśli chcesz odwołać się do zasobu, który nie jest właścicielem, ale nie wiesz, czy zasób przeżyje obiekt, który się do niego odwołuje, spakuj go shared_ptri użyj weak_ptr- możesz przetestować, czy rodzic shared_ptrżyje lock, co spowoduje zwraca wartość inną shared_ptrniż null, jeśli zasób nadal istnieje. Jeśli chcesz sprawdzić, czy zasób jest martwy, użyj expired. Oba mogą brzmieć podobnie, ale są bardzo różne w obliczu równoczesnego wykonywania, ponieważ expiredgwarantuje tylko wartość zwracaną dla tej pojedynczej instrukcji. Pozornie niewinny test

if(!wptr.expired())
  something_assuming_the_resource_is_still_alive();

jest potencjalnym warunkiem wyścigu.

Xeo
źródło
1
W przypadku braku własności, prawdopodobnie powinieneś preferować odniesienia do wskaźników, chyba że nie potrzebujesz własności i możliwości resetowania tam, gdzie odniesienia nie będą go wycinać, nawet wtedy możesz rozważyć przepisanie oryginalnego obiektu na a, shared_ptra nie posiadanie wskaźnika na a weak_ptr...
David Rodríguez - dribeas
2
Nie miałem na myśli odniesienia do wskaźnika , ale odniesienie zamiast wskaźnika. Jeśli nie ma własności, chyba że potrzebujesz resetowalności (lub zerowania, ale zerowanie bez możliwości resetowania byłoby dość ograniczone), możesz w pierwszej kolejności użyć zwykłego odniesienia, a nie wskaźnika.
David Rodríguez - dribeas
1
@David: Ach, rozumiem. :) Tak, referencje nie są złe, w tych przypadkach osobiście wolę je. Dodam je.
Xeo
1
@Xeo: shared_array<T>jest alternatywą dla shared_ptr<T[]>nie shared_ptr<vector<T>>: nie może rosnąć.
R. Martinho Fernandes,
1
@GregroyCurrie: To jest ... dokładnie to, co napisałem? Powiedziałem, że to przykład potencjalnej sytuacji wyścigowej.
Xeo,
127

Decyzja o wyborze inteligentnego wskaźnika jest kwestią własności . Jeśli chodzi o zarządzanie zasobami, obiekt A jest właścicielem obiektu B, jeśli kontroluje on czas życia obiektu B. Na przykład zmienne składowe są własnością ich odpowiednich obiektów, ponieważ czas życia zmiennych składowych jest powiązany z czasem życia obiektu. Wybierz inteligentne wskaźniki na podstawie tego, jak obiekt jest własnością.

Pamiętaj, że własność w systemie oprogramowania jest oddzielna od własności, tak jakbyśmy myśleli o tym poza oprogramowaniem. Na przykład osoba może „posiadać” swój dom, ale to niekoniecznie oznacza, że Personobiekt ma kontrolę nad czasem życia Houseobiektu. Połączenie tych rzeczywistych koncepcji z koncepcjami oprogramowania to niezawodny sposób na zaprogramowanie się w dziurze.


Jeśli masz wyłączną własność obiektu, użyj std::unique_ptr<T>.

Jeśli masz wspólną własność obiektu ...
- Jeśli nie ma cykli własności, użyj std::shared_ptr<T>.
- Jeśli występują cykle, zdefiniuj „kierunek” i używaj std::shared_ptr<T>w jednym kierunku i std::weak_ptr<T>w drugim.

Jeśli obiekt należy do Ciebie, ale istnieje prawdopodobieństwo, że nie masz właściciela, użyj normalnych wskaźników T*(np. Wskaźników nadrzędnych).

Jeśli obiekt jest Twoją własnością (lub w inny sposób gwarantuje istnienie), użyj referencji T&.


Uwaga: należy pamiętać o kosztach inteligentnych wskaźników. W środowiskach o ograniczonej pamięci lub wydajności korzystne może być użycie zwykłych wskaźników z bardziej ręcznym schematem zarządzania pamięcią.

Koszty:

  • Jeśli masz niestandardowy program usuwający (np. Korzystasz z pul alokacji), spowoduje to narzut na wskaźnik, którego można łatwo uniknąć poprzez ręczne usunięcie.
  • std::shared_ptrma narzut przyrostu liczby referencyjnej kopii, plus zmniejszenie zniszczenia, po którym następuje sprawdzenie 0-krotności z usunięciem trzymanego obiektu. W zależności od implementacji może to nadmuchać kod i spowodować problemy z wydajnością.
  • Czas kompilacji. Podobnie jak w przypadku wszystkich szablonów, inteligentne wskaźniki mają negatywny wpływ na czasy kompilacji.

Przykłady:

struct BinaryTree
{
    Tree* m_parent;
    std::unique_ptr<BinaryTree> m_children[2]; // or use std::array...
};

Drzewo binarne nie jest właścicielem swojego rodzica, ale istnienie drzewa implikuje istnienie jego rodzica (lub nullptrdla katalogu głównego), więc używa normalnego wskaźnika. Drzewo binarne (z semantyką wartości) ma wyłączną własność swoich dzieci, więc tak jest std::unique_ptr.

struct ListNode
{
    std::shared_ptr<ListNode> m_next;
    std::weak_ptr<ListNode> m_prev;
};

Tutaj węzeł listy posiada swoje następne i poprzednie listy, więc określamy kierunek i używamy shared_ptrdo następnego i weak_ptrpoprzedniego, aby przerwać cykl.

Peter Alexander
źródło
3
Na przykład drzewa binarnego niektórzy sugerowaliby używanie go shared_ptr<BinaryTree>dla dzieci i weak_ptr<BinaryTree>dla relacji rodziców.
David Rodríguez - dribeas
@ DavidRodríguez-dribeas: Zależy, czy Drzewo ma semantykę wartości, czy nie. Jeśli ludzie będą odwoływać się do twojego drzewa zewnętrznie, nawet gdy drzewo źródłowe zostanie zniszczone, to tak, najlepsza będzie kombinacja dzielonego / słabego wskaźnika.
Peter Alexander
Jeśli obiekt jest Twoją własnością i istnieje gwarancja, że ​​istnieje, to dlaczego nie odniesienie.
Martin York,
1
Jeśli użyjesz odniesienia, nie możesz nigdy zmienić elementu nadrzędnego, co może, ale nie musi, utrudnić projektu. Utrudnia to równoważenie drzew.
Mooing Duck
3
+1, ale w pierwszej linii należy dodać definicję „własności”. Często muszę wyraźnie stwierdzić, że chodzi o życie i śmierć obiektu, a nie o własność w bardziej specyficznym dla domeny znaczeniu.
Klaim
19

Używaj przez unique_ptr<T>cały czas, z wyjątkiem sytuacji, gdy potrzebujesz zliczania referencji, w takim przypadku używaj shared_ptr<T>(i w bardzo rzadkich przypadkach, weak_ptr<T>aby zapobiec cyklom referencyjnym). W prawie każdym przypadku przeniesienie unikalnej własności jest w porządku.

Nieprzetworzone wskaźniki: Dobre tylko wtedy, gdy potrzebujesz zwrotów kowariantnych, co może się nie zdarzyć. W przeciwnym razie nie są strasznie przydatne.

Wskaźniki tablic: unique_ptrma specjalizację, dla T[]której automatycznie wywołuje delete[]wynik, więc możesz unique_ptr<int[]> p(new int[42]);na przykład bezpiecznie to zrobić . shared_ptrnadal potrzebujesz niestandardowego usuwacza, ale nie potrzebujesz specjalistycznego wspólnego lub unikalnego wskaźnika tablicy. Oczywiście takie rzeczy i tak najlepiej najlepiej zastąpić std::vector. Niestety shared_ptrnie zapewnia funkcji dostępu do tablicy, więc nadal będziesz musiał ręcznie wywoływać get(), ale unique_ptr<T[]>zapewnia operator[]zamiast operator*i operator->. W każdym razie musisz sam sprawdzić granice. To sprawia, że ​​jest shared_ptrnieco mniej przyjazny dla użytkownika, choć zapewne ogólna przewaga i brak zależności od wzmocnienia, unique_ptra shared_ptrzwycięzcy ponownie.

Wskaźniki o zasięgu: Nieistotne unique_ptr, podobnie jakauto_ptr .

Naprawdę nie ma w tym nic więcej. W C ++ 03 bez semantyki ruchu sytuacja ta była bardzo skomplikowana, ale w C ++ 11 rada jest bardzo prosta.

Nadal istnieją zastosowania innych inteligentnych wskaźników, takich jak intrusive_ptrlub interprocess_ptr. Są jednak bardzo niszowe i całkowicie niepotrzebne w ogólnym przypadku.

Szczeniak
źródło
Również surowe wskaźniki dla iteracji. I dla buforów parametrów wyjściowych, gdzie bufor jest własnością osoby wywołującej.
Ben Voigt
Hmm, tak to czytam, są to sytuacje, które są zarówno zwrotem kowariantnym, jak i nie posiadaniem. Przepisanie może być dobre, jeśli masz na myśli związek, a nie skrzyżowanie. Powiedziałbym również, że warto również wspomnieć o iteracji.
Ben Voigt
2
std::unique_ptr<T[]>zapewnia operator[]zamiast operator*i operator->. Prawdą jest jednak, że nadal musisz sprawdzić się samodzielnie.
Xeo
8

Przypadki użycia unique_ptr:

  • Metody fabryczne
  • Członkowie, którzy są wskaźnikami (w tym pimpl)
  • Przechowywanie wskaźników w stl containters (aby uniknąć ruchów)
  • Wykorzystanie dużych lokalnych obiektów dynamicznych

Przypadki użycia shared_ptr:

  • Udostępnianie obiektów w wątkach
  • Ogólne udostępnianie obiektów

Przypadki użycia weak_ptr:

  • Duża mapa, która działa jako ogólne odniesienie (np. Mapa wszystkich otwartych gniazd)

Możesz edytować i dodawać więcej

Lalaland
źródło
Naprawdę podoba mi się twoja odpowiedź, gdy podajesz scenariusze.
Nicholas Humphrey