Używanie inteligentnych wskaźników dla członków klasy


159

Mam problem ze zrozumieniem użycia inteligentnych wskaźników jako członków klasy w C ++ 11. Dużo czytałem o inteligentnych wskazówkach i myślę, że rozumiem, jak unique_ptri shared_ptr/ weak_ptrdziała ogólnie. To, czego nie rozumiem, to prawdziwe użycie. Wygląda na to, że wszyscy zalecają używanie unique_ptrgo prawie cały czas. Ale jak bym zaimplementował coś takiego:

class Device {
};

class Settings {
    Device *device;
public:
    Settings(Device *device) {
        this->device = device;
    }

    Device *getDevice() {
        return device;
    }
};    

int main() {
    Device *device = new Device();
    Settings settings(device);
    // ...
    Device *myDevice = settings.getDevice();
    // do something with myDevice...
}

Powiedzmy, że chciałbym zastąpić wskaźniki inteligentnymi wskazówkami. A unique_ptrnie zadziała z powodu getDevice(), prawda? Więc to jest czas, kiedy używam shared_ptri weak_ptr? Nie ma możliwości korzystania unique_ptr? Wydaje mi się, że w większości przypadków shared_ptrma to większy sens, chyba że używam wskaźnika w naprawdę małym teleskopie?

class Device {
};

class Settings {
    std::shared_ptr<Device> device;
public:
    Settings(std::shared_ptr<Device> device) {
        this->device = device;
    }

    std::weak_ptr<Device> getDevice() {
        return device;
    }
};

int main() {
    std::shared_ptr<Device> device(new Device());
    Settings settings(device);
    // ...
    std::weak_ptr<Device> myDevice = settings.getDevice();
    // do something with myDevice...
}

Czy to jest droga? Dziękuję bardzo!


4
Pomaga jasno określić czas życia, własność i możliwe zerowe wartości. Na przykład, po przejściu devicedo konstruktora programu settings, czy nadal chcesz móc odwoływać się do niego w zakresie wywołującym, czy tylko za pośrednictwem settings? Jeśli to drugie, unique_ptrjest przydatne. Ponadto, czy masz scenariusz, w którym wartość zwracana getDevice()to null. Jeśli nie, po prostu zwróć odwołanie.
Keith

2
Tak, a shared_ptrjest poprawne w 8/10 przypadków. Pozostałe 2/10 są podzielone między unique_ptri weak_ptr. Jest również weak_ptrogólnie używany do przerywania odwołań cyklicznych; Nie jestem pewien, czy twoje użycie zostanie uznane za prawidłowe.
Collin Dauphinee

2
Przede wszystkim, jakiej własności chcesz dla deviceczłonka danych? Najpierw musisz o tym zdecydować.
juanchopanza

1
Ok, rozumiem, że jako dzwoniący mógłbym unique_ptrzamiast tego użyć a i zrezygnować z własności podczas dzwonienia do konstruktora, jeśli wiem, że na razie nie będę go już potrzebować. Ale jako projektant Settingsklasy nie wiem, czy dzwoniący również chce zachować odniesienie. Może urządzenie znajdzie zastosowanie w wielu miejscach. Ok, może to jest dokładnie twój punkt widzenia. W takim przypadku nie byłbym jedynym właścicielem i chyba wtedy użyłbym shared_ptr. A więc: inteligentne punkty zastępują wskaźniki, ale nie odniesienia, prawda?
michaelk

this-> device = device; Użyj również list inicjalizacyjnych.
Nils

Odpowiedzi:


202

A unique_ptrnie zadziała z powodu getDevice(), prawda?

Nie, niekoniecznie. Ważne jest tutaj określenie odpowiedniej polityki własności dla twojego Deviceobiektu, tj. Kto będzie właścicielem obiektu wskazywanego przez twój (inteligentny) wskaźnik.

Czy będzie to instancja Settingsobiektu sam ? Czy Deviceobiekt będzie musiał zostać zniszczony automatycznie, gdy Settingszostanie zniszczony, czy też powinien przeżyć ten obiekt?

W pierwszym przypadku std::unique_ptrjest to, czego potrzebujesz, ponieważ staje Settingssię jedynym (unikalnym) właścicielem wskazanego przedmiotu i jedynym obiektem odpowiedzialnym za jego zniszczenie.

Przy takim założeniu getDevice()powinien zwrócić prosty wskaźnik obserwacyjny (wskaźniki obserwacyjne to wskaźniki, które nie utrzymują wskazanego obiektu przy życiu). Najprostszym rodzajem wskaźnika obserwacyjnego jest wskaźnik surowy:

#include <memory>

class Device {
};

class Settings {
    std::unique_ptr<Device> device;
public:
    Settings(std::unique_ptr<Device> d) {
        device = std::move(d);
    }

    Device* getDevice() {
        return device.get();
    }
};

int main() {
    std::unique_ptr<Device> device(new Device());
    Settings settings(std::move(device));
    // ...
    Device *myDevice = settings.getDevice();
    // do something with myDevice...
}

[ UWAGA 1: Możesz się zastanawiać, dlaczego używam tutaj wskaźników surowych, kiedy wszyscy powtarzają, że wskaźniki surowe są złe, niebezpieczne i niebezpieczne. W rzeczywistości jest to cenne ostrzeżenie, ale ważne jest, aby umieścić je w odpowiednim kontekście: surowe wskaźniki są złe, gdy są używane do wykonywania ręcznego zarządzania pamięcią , tj. Przydzielania i zwalniania obiektów za pomocą newi delete. Kiedy używa się go wyłącznie jako środka do osiągnięcia semantyki referencyjnej i przekazywania nie-posiadania, obserwowania wskaźników, nie ma nic z natury niebezpiecznego w surowych wskaźnikach, może z wyjątkiem faktu, że należy uważać, aby nie wyłuskać wiszącego wskaźnika. - UWAGA KOŃCOWA 1 ]

[ UWAGA 2: Jak okazało się w komentarzach, w tym konkretnym przypadku, gdy własność jest unikalna, a posiadany obiekt jest zawsze obecny (tj. Wewnętrzny element danych devicenigdy nie będzie nullptr), funkcja getDevice()mogłaby (i być może powinna) zwraca raczej odwołanie niż wskaźnik. Chociaż to prawda, zdecydowałem się zwrócić tutaj surowy wskaźnik, ponieważ chciałem, aby była to krótka odpowiedź, którą można uogólnić na przypadek, w którym devicemoże być nullptr, i pokazać, że surowe wskaźniki są w porządku, o ile nie używa się ich do ręczne zarządzanie pamięcią. - UWAGA KOŃCOWA 2 ]


Sytuacja jest oczywiście radykalnie inna, jeśli twój Settingsprzedmiot nie powinien mieć wyłącznej własności urządzenia. Może tak być na przykład w przypadku, gdy zniszczenie Settingsprzedmiotu nie powinno oznaczać również zniszczenia wskazanego Deviceprzedmiotu.

To jest coś, co tylko Ty jako projektant swojego programu możesz powiedzieć; z podanego przez ciebie przykładu trudno mi stwierdzić, czy tak jest, czy nie.

Aby pomóc ci to zrozumieć, możesz zadać sobie pytanie, czy istnieją inne obiekty oprócz tych, Settingsktóre są uprawnione do utrzymywania Deviceobiektu przy życiu, o ile trzymają na nim wskaźnik, zamiast być biernymi obserwatorami. Jeśli tak jest, potrzebujesz polityki współwłasności , która std::shared_ptrzapewnia:

#include <memory>

class Device {
};

class Settings {
    std::shared_ptr<Device> device;
public:
    Settings(std::shared_ptr<Device> const& d) {
        device = d;
    }

    std::shared_ptr<Device> getDevice() {
        return device;
    }
};

int main() {
    std::shared_ptr<Device> device = std::make_shared<Device>();
    Settings settings(device);
    // ...
    std::shared_ptr<Device> myDevice = settings.getDevice();
    // do something with myDevice...
}

Zwróć uwagę, że weak_ptrjest to wskaźnik obserwujący , a nie wskaźnik będący właścicielem - innymi słowy, nie utrzymuje on przy życiu wskazanego obiektu, jeśli wszystkie inne wskaźniki będące właścicielami wskazanego obiektu wykraczają poza zakres.

Przewaga weak_ptrnad regularnym surowego wskaźnik jest to, że można bezpiecznie powiedzieć, czy weak_ptrjest zwisające czy nie (czyli czy jest wskazując ważnego obiektu, lub jeśli obiekt pierwotnie wskazał został zniszczony). Można to zrobić, wywołując expired()funkcję składową na weak_ptrobiekcie.


4
@LKK: Tak, zgadza się. A weak_ptrjest zawsze alternatywą dla surowych wskaźników obserwacyjnych. W pewnym sensie jest to bezpieczniejsze, ponieważ przed wyłuskiwaniem można sprawdzić, czy nie zwisa, ale wiąże się to również z pewnym narzutem. Jeśli możesz łatwo zagwarantować, że nie zamierzasz wyłuskać wiszącego wskaźnika, powinieneś być w porządku z obserwowaniem surowych wskaźników
Andy Prowl

6
W pierwszym przypadku prawdopodobnie byłoby nawet lepiej getDevice(), gdybyśmy zwrócili odniesienie, prawda? Więc dzwoniący nie musiałby sprawdzać nullptr.
vobject

5
@chico: Nie wiem, co masz na myśli. auto myDevice = settings.getDevice()utworzy nowe wystąpienie typu o Devicenazwie myDevicei skopiuje je z tego, do którego odwołuje się odwołanie, które getDevice()zwraca. Jeśli chcesz myDevicebyć punktem odniesienia, musisz to zrobić auto& myDevice = settings.getDevice(). Więc jeśli czegoś nie brakuje, jesteśmy z powrotem w tej samej sytuacji, w której mieliśmy bez użycia auto.
Andy Prowl

2
@Purrformance: Ponieważ nie chcesz oddawać własności obiektu - przekazanie unique_ptrklientowi elementu, który można modyfikować, otwiera możliwość, że klient się z niego przeniesie, uzyskując w ten sposób własność i pozostawiając pusty (unikalny) wskaźnik.
Andy Prowl

7
@Purrformance: Chociaż powstrzymałoby to klienta przed przeprowadzką (chyba że klient jest szalonym naukowcem, który lubi const_casts), osobiście nie zrobiłbym tego. Ujawnia szczegół implementacji, tj. Fakt, że własność jest wyjątkowa i realizowana poprzez unique_ptr. Widzę to w ten sposób: jeśli chcesz / musisz przekazać / zwrócić własność, przekaż / zwróć inteligentny wskaźnik ( unique_ptrlub shared_ptr, w zależności od rodzaju własności). Jeśli nie chcesz / nie musisz przekazywać / zwracać własności, użyj (odpowiednio constzakwalifikowanego) wskaźnika lub odwołania, głównie w zależności od tego, czy argument może mieć wartość null, czy nie.
Andy Prowl

0
class Device {
};

class Settings {
    std::shared_ptr<Device> device;
public:
    Settings(const std::shared_ptr<Device>& device) : device(device) {

    }

    const std::shared_ptr<Device>& getDevice() {
        return device;
    }
};

int main()
{
    std::shared_ptr<Device> device(new Device());
    Settings settings(device);
    // ...
    std::shared_ptr<Device> myDevice(settings.getDevice());
    // do something with myDevice...
    return 0;
}

week_ptrjest używany tylko dla pętli referencyjnych. Graf zależności musi być grafem skierowanym acyklicznie. We wspólnych wskaźnikach istnieją 2 liczniki odwołań: 1 dla shared_ptrs i 1 dla wszystkich wskaźników ( shared_ptri weak_ptr). Po shared_ptrusunięciu wszystkich znaków wskaźnik jest usuwany. Gdy potrzebny jest wskaźnik from weak_ptr, lockpowinien zostać użyty do pobrania wskaźnika, jeśli istnieje.


Więc jeśli dobrze rozumiem twoją odpowiedź, inteligentne wskaźniki zastępują surowe wskaźniki, ale niekoniecznie odniesienia?
michaelk

Czy faktycznie istnieją dwie liczby referencji w shared_ptr? Czy możesz wyjaśnić dlaczego? O ile rozumiem, weak_ptrnie musi być liczony, ponieważ po prostu tworzy nowy shared_ptrpodczas działania na obiekcie (jeśli podstawowy obiekt nadal istnieje).
Björn Pollex

@ BjörnPollex: Stworzyłem dla Ciebie krótki przykład: link . Nie zaimplementowałem wszystkiego, tylko konstruktory kopiujące i lock. Wersja boost jest również bezpieczna wątkowo przy zliczaniu referencji ( deletewywoływana jest tylko raz).
Naszta

@Naszta: Twój przykład pokazuje, że można to zaimplementować przy użyciu dwóch liczb referencyjnych, ale Twoja odpowiedź sugeruje, że jest to wymagane , a nie sądzę, że jest. Czy mógłbyś to wyjaśnić w swojej odpowiedzi?
Björn Pollex

1
@ BjörnPollex, weak_ptr::lock()aby stwierdzić, czy obiekt wygasł, musi sprawdzić „blok kontrolny”, który zawiera pierwszą liczbę referencji i wskaźnik do obiektu, więc blok sterujący nie może zostać zniszczony, gdy są jakieś weak_ptrobiekty nadal w użyciu, więc liczba weak_ptrobiektów musi być śledzona, co robi drugi licznik odwołań. Obiekt zostaje zniszczony, gdy pierwsza liczba ref spadnie do zera, blok kontrolny zostanie zniszczony, gdy druga wartość ref spadnie do zera.
Jonathan Wakely
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.