raw, poor_ptr, unique_ptr, shared_ptr itp… Jak mądrze je wybrać?

33

W C ++ jest wiele wskazówek, ale szczerze mówiąc za około 5 lat w programowaniu w C ++ (szczególnie w Qt Framework) używam tylko starego surowego wskaźnika:

SomeKindOfObject *someKindOfObject = new SomeKindOfObject();

Wiem, że istnieje wiele innych „inteligentnych” wskaźników:

// shared pointer:
shared_ptr<SomeKindofObject> Object;

// unique pointer:
unique_ptr<SomeKindofObject> Object;

// weak pointer:
weak_ptr<SomeKindofObject> Object;

Ale nie mam najmniejszego pojęcia, co z nimi zrobić i co mogą mi zaoferować w porównaniu z surowymi wskazówkami.

Na przykład mam ten nagłówek klasy:

#ifndef LIBRARY
#define LIBRARY

class LIBRARY
{
public:
    // Permanent list that will be updated from time to time where
    // each items can be modified everywhere in the code:
    QList<ItemThatWillBeUsedEveryWhere*> listOfUselessThings; 
private:
    // Temporary reader that will read something to put in the list
    // and be quickly deleted:
    QSettings *_reader;
    // A dialog that will show something (just for the sake of example):
    QDialog *_dialog;
};

#endif 

Nie jest to oczywiście wyczerpujące, ale czy dla każdego z tych 3 wskaźników można pozostawić je „surowe”, czy powinienem użyć czegoś bardziej odpowiedniego?

I po raz drugi, jeśli pracodawca przeczyta kod, czy będzie surowy w kwestii jakich wskaźników używam, czy nie?

CheshireChild
źródło
Ten temat wydaje się odpowiedni dla SO. to było w 2008 roku . A oto jakiego rodzaju wskaźnika używam, kiedy? . Jestem pewien, że znajdziesz jeszcze lepsze dopasowania. Były to tylko pierwszy widziałem
sehe
imo granice tego, ponieważ dotyczy to zarówno koncepcyjnego znaczenia / intencji tych klas, jak i technicznych szczegółów ich zachowania i realizacji. Ponieważ zaakceptowana odpowiedź skłania się ku temu pierwszemu, cieszę się, że jest to „wersja PSE” tego SO pytania.
Ixrec

Odpowiedzi:

70

„Surowy” wskaźnik nie jest zarządzany. Oznacza to, że następujący wiersz:

SomeKindOfObject *someKindOfObject = new SomeKindOfObject();

... wycieknie pamięć, jeśli towarzyszenie deletenie zostanie wykonane we właściwym czasie.

auto_ptr

W celu zminimalizowania tych przypadków std::auto_ptr<>wprowadzono. Jednak ze względu na ograniczenia C ++ przed standardem 2011, nadal bardzo łatwo auto_ptrjest przeciekać pamięć. Jest to wystarczające w ograniczonych przypadkach, takich jak ten, jednak:

void func() {
    std::auto_ptr<SomeKindOfObject> sKOO_ptr(new SomeKindOfObject());
    // do some work
    // will not leak if you do not copy sKOO_ptr.
}

Jednym z najsłabszych przypadków użycia są pojemniki. Wynika to z faktu, że jeśli auto_ptr<>zostanie wykonana kopia pliku, a stara kopia nie zostanie starannie zresetowana, pojemnik może usunąć wskaźnik i utracić dane.

unique_ptr

Jako zamiennik C ++ 11 wprowadził std::unique_ptr<>:

void func2() {
    std::unique_ptr<SomeKindofObject> sKOO_unique(new SomeKindOfObject());

    func3(sKOO_unique); // now func3() owns the pointer and sKOO_unique is no longer valid
}

Taki unique_ptr<>zostanie poprawnie wyczyszczony, nawet jeśli zostanie przekazany między funkcjami. Robi to poprzez semantyczne reprezentowanie „własności” wskaźnika - „właściciel” go oczyszcza. Dzięki temu idealnie nadaje się do stosowania w pojemnikach:

std::vector<std::unique_ptr<SomeKindofObject>> sKOO_vector();

W przeciwieństwie do auto_ptr<>, unique_ptr<>jest tu dobrze zachowany, a po vectorzmianie rozmiaru żaden z obiektów nie zostanie przypadkowo usunięty podczas vectorkopiowania jego magazynu kopii zapasowych.

shared_ptr i weak_ptr

unique_ptr<>jest to użyteczne, oczywiście, ale zdarzają się przypadki, w których dwie części bazy kodu mogą odnosić się do tego samego obiektu i kopiować wskaźnik dookoła, przy jednoczesnym zagwarantowaniu prawidłowego czyszczenia. Na przykład drzewo może wyglądać tak, gdy używasz std::shared_ptr<>:

template<class T>
struct Node {
    T value;
    std::shared_ptr<Node<T>> left;
    std::shared_ptr<Node<T>> right;
};

W takim przypadku możemy nawet zatrzymać wiele kopii węzła głównego, a drzewo zostanie odpowiednio wyczyszczone, gdy wszystkie kopie węzła głównego zostaną zniszczone.

Działa to, ponieważ każdy shared_ptr<>zachowuje nie tylko wskaźnik do obiektu, ale także liczbę referencji wszystkich shared_ptr<>obiektów, które odnoszą się do tego samego wskaźnika. Po utworzeniu nowego liczba rośnie. Kiedy ktoś zostanie zniszczony, liczba spada. Gdy liczba osiągnie zero, wskaźnik będzie mieć wartość deleted.

To wprowadza problem: struktury podwójnie połączone kończą się referencjami kołowymi. Powiedzmy, że chcemy dodać parentwskaźnik do naszego drzewa Node:

template<class T>
struct Node {
    T value;
    std::shared_ptr<Node<T>> parent;
    std::shared_ptr<Node<T>> left;
    std::shared_ptr<Node<T>> right;
};

Teraz, jeśli usuniemy Node, istnieje cykliczne odniesienie do niego. Nigdy nie będzie deleted, ponieważ jego liczba referencyjna nigdy nie będzie równa zero.

Aby rozwiązać ten problem, użyj std::weak_ptr<>:

template<class T>
struct Node {
    T value;
    std::weak_ptr<Node<T>> parent;
    std::shared_ptr<Node<T>> left;
    std::shared_ptr<Node<T>> right;
};

Teraz wszystko będzie działać poprawnie, a usunięcie węzła nie pozostawi zablokowanych odniesień do węzła nadrzędnego. Jednak sprawia, że ​​chodzenie po drzewie jest nieco bardziej skomplikowane:

std::shared_ptr<Node<T>> parent_of_this = node->parent.lock();

W ten sposób możesz zablokować odniesienie do węzła i masz uzasadnioną gwarancję, że nie zniknie ono podczas pracy nad nim, ponieważ trzymasz się jednego shared_ptr<>z nich.

make_shared i make_unique

Teraz, istnieją pewne drobne problemy z shared_ptr<>i unique_ptr<>że należy się zająć. Problem mają następujące dwie linie:

foo_unique(std::unique_ptr<SomeKindofObject>(new SomeKindOfObject()), thrower());
foo_shared(std::shared_ptr<SomeKindofObject>(new SomeKindOfObject()), thrower());

Jeśli thrower()zgłosi wyjątek, obie linie wyciekną pamięć. Co więcej, shared_ptr<>liczba referencyjna utrzymuje się z dala od obiektu, na który wskazuje, a to może oznaczać drugi przydział). To zwykle nie jest pożądane.

C ++ 11 zapewnia, std::make_shared<>()a C ++ 14 zapewnia std::make_unique<>()rozwiązanie tego problemu:

foo_unique(std::make_unique<SomeKindofObject>(), thrower());
foo_shared(std::make_shared<SomeKindofObject>(), thrower());

Teraz, w obu przypadkach, nawet jeśli thrower()zgłasza wyjątek, nie nastąpi wyciek pamięci. Jako bonus, make_shared<>()ma możliwość utworzenia liczby referencyjnej w tym samym obszarze pamięci, co obiekt zarządzany, który może być zarówno szybszy, jak i może zaoszczędzić kilka bajtów pamięci, dając jednocześnie wyjątkową gwarancję bezpieczeństwa!

Uwagi na temat Qt

Należy jednak zauważyć, że Qt, który musi obsługiwać kompilatory w wersjach wcześniejszych niż C ++ 11, ma swój własny model zbierania elementów bezużytecznych: wiele z QObjectnich ma mechanizm, w którym zostaną one odpowiednio zniszczone bez potrzeby korzystania z deletenich przez użytkownika .

Nie wiem, jak QObjectbędzie się zachowywać, gdy będzie zarządzany przez wskaźniki zarządzane przez C ++ 11, więc nie mogę powiedzieć, że shared_ptr<QDialog>to dobry pomysł. Nie mam wystarczającego doświadczenia z Qt, aby powiedzieć na pewno, ale wierzę, że Qt5 został dostosowany do tego przypadku użycia.

greyfade
źródło
1
@Zilators: Proszę zwrócić uwagę na mój dodany komentarz na temat Qt. Odpowiedź na pytanie, czy należy zarządzać wszystkimi trzema wskaźnikami, zależy od tego, czy obiekty Qt będą się dobrze zachowywać.
greyfade
2
„obaj dokonują osobnego przydziału, aby utrzymać wskaźnik”? Nie, unikalny_ptr nigdy nie przydziela niczego dodatkowego, tylko współużytkowany_ptr musi przydzielić liczbę referencyjną + obiekt-przydział. „obie linie wyciekną pamięć”? nie, może tylko nie gwarantować złego zachowania.
Deduplicator
1
@Deduplicator: Moje sformułowanie musiało być niejasne: shared_ptrJest to osobny obiekt - oddzielny przydział - od newobiektu ed. Istnieją w różnych lokalizacjach. make_sharedma możliwość złożenia ich w jednym miejscu, co poprawia między innymi lokalizację pamięci podręcznej.
greyfade
2
@greyfade: Nononono. shared_ptrjest przedmiotem. Aby zarządzać obiektem, musi on przydzielić obiekt (liczba referencyjna (słaby + silny) + niszczyciel). make_sharedumożliwia przydzielenie tego i zarządzanego obiektu jako jednego elementu. unique_ptrnie korzysta z nich, więc nie ma żadnej odpowiedniej korzyści, oprócz upewnienia się, że obiekt jest zawsze własnością inteligentnego wskaźnika. Nawiasem mówiąc , można mieć obiekt, shared_ptrktóry jest właścicielem obiektu bazowego i reprezentuje nullptr, lub który nie jest właścicielem i reprezentuje wskaźnik inny niż null.
Deduplicator
1
Spojrzałem na to i wydaje się, że istnieje ogólne zamieszanie co do tego, co shared_ptrrobi: 1. Dzieli własność jakiegoś obiektu (reprezentowanego przez wewnętrzny dynamicznie przydzielany obiekt o słabej i silnej liczbie referencyjnej, a także usuwający) . 2. Zawiera wskaźnik. Te dwie części są niezależne. make_uniquei make_sharedoba upewnij się, że przydzielony obiekt jest bezpiecznie umieszczony w inteligentnym wskaźniku. Ponadto make_sharedpozwala przydzielić obiekt własności i zarządzany wskaźnik razem.
Deduplicator