Zaletą używania std::unique_ptr<T>
(poza tym, że nie trzeba pamiętać o wywołaniu delete
lub delete[]
jawnie) jest to, że gwarantuje ono, że wskaźnik jest albo nullptr
wskazuje na prawidłową instancję (bazowego) obiektu. Wrócę do tego po tym, jak odpowiedzieć na to pytanie, ale pierwsza wiadomość jest DO używać inteligentne kursory zarządzać żywotność dynamicznie alokowanych obiektów.
Teraz twój problem polega na tym, jak użyć tego ze starym kodem .
Moja sugestia jest taka, że jeśli nie chcesz przenosić lub współdzielić własności, zawsze powinieneś przekazywać odniesienia do obiektu. Zadeklaruj swoją funkcję w ten sposób (z const
kwalifikatorami lub bez , w razie potrzeby):
bool func(BaseClass& ref, int other_arg) { ... }
Następnie wywołujący, który ma a std::shared_ptr<BaseClass> ptr
, albo obsłuży nullptr
sprawę, albo poprosi bool func(...)
o obliczenie wyniku:
if (ptr) {
result = func(*ptr, some_int);
} else {
}
Oznacza to, że każdy wywołujący musi obiecać, że odwołanie jest prawidłowe i że będzie nadal obowiązywać przez cały czas wykonywania treści funkcji.
Oto powód, dla którego ja mocno wierzę, że powinien nie przechodzą surowe wskazówek lub odniesienia do inteligentnych wskaźników.
Surowy wskaźnik to tylko adres pamięci. Może mieć jedno z (co najmniej) 4 znaczeń:
- Adres bloku pamięci, w którym znajduje się żądany obiekt. ( dobra )
- Adres 0x0, co do którego możesz być pewien, nie jest dereferencjonowalny i może mieć semantykę „nic” lub „brak obiektu”. ( zły )
- Adres bloku pamięci, który znajduje się poza przestrzenią adresowalną twojego procesu (wyłuskiwanie go, miejmy nadzieję, spowoduje awarię programu). ( brzydki )
- Adres bloku pamięci, który można wyłuskać, ale który nie zawiera tego, czego oczekujesz. Być może wskaźnik został przypadkowo zmodyfikowany i teraz wskazuje na inny zapisywalny adres (zupełnie innej zmiennej w twoim procesie). Zapisywanie do tego miejsca w pamięci sprawi, że czasami będzie dużo zabawy podczas wykonywania, ponieważ system operacyjny nie będzie narzekać, dopóki możesz tam pisać. ( Zoinks! )
Prawidłowe użycie inteligentnych wskaźników łagodzi raczej przerażające przypadki 3 i 4, które zwykle nie są wykrywalne w czasie kompilacji i których zwykle doświadczasz tylko w czasie wykonywania, gdy program ulega awarii lub wykonuje nieoczekiwane rzeczy.
Przekazywanie inteligentnych wskaźników jako argumentów ma dwie wady: nie można zmienić const
-ness wskazanego obiektu bez wykonania kopii (co dodaje narzut shared_ptr
i nie jest możliwe unique_ptr
) i nadal pozostaje znaczenie second ( nullptr
).
Drugi przypadek oznaczyłem jako ( zły ) z punktu widzenia projektowania. To jest bardziej subtelny argument dotyczący odpowiedzialności.
Wyobraź sobie, co to znaczy, gdy funkcja otrzymuje nullptr
parametr a. Najpierw musi zdecydować, co z tym zrobić: użyć „magicznej” wartości w miejsce brakującego obiektu? całkowicie zmienić zachowanie i obliczyć coś innego (co nie wymaga obiektu)? panikować i rzucić wyjątek? Co więcej, co się dzieje, gdy funkcja przyjmuje 2, 3 lub nawet więcej argumentów za pomocą surowego wskaźnika? Musi sprawdzić każdą z nich i odpowiednio dostosować swoje zachowanie. To dodaje zupełnie nowy poziom do walidacji danych wejściowych bez żadnego powodu.
Dzwoniący powinien być tym, który ma wystarczającą ilość informacji kontekstowych, aby podjąć te decyzje, lub innymi słowy, im więcej wiesz , zło jest mniej przerażające. Z drugiej strony, funkcja powinna po prostu przyjąć obietnicę dzwoniącego, że pamięć, na którą jest wskazywana, jest bezpieczna w użyciu zgodnie z przeznaczeniem. (Odniesienia są nadal adresami pamięci, ale koncepcyjnie stanowią obietnicę ważności).
std::unique_ptr
zastd::vector<std::unique_ptr>
kłótnię?