Mam pierwszą próbę użycia C ++ 11 unique_ptr
; Zastępuję polimorficzny surowy wskaźnik wewnątrz mojego projektu, który jest własnością jednej klasy, ale jest często przekazywany.
Kiedyś miałem takie funkcje, jak:
bool func(BaseClass* ptr, int other_arg) {
bool val;
// plain ordinary function that does something...
return val;
}
Ale szybko zdałem sobie sprawę, że nie będę w stanie przełączyć się na:
bool func(std::unique_ptr<BaseClass> ptr, int other_arg);
Ponieważ wywołujący musiałby obsługiwać własność wskaźnika do funkcji, czego nie chcę. Więc jakie jest najlepsze rozwiązanie mojego problemu?
Pomyślałem o przekazaniu wskaźnika jako odniesienia, w ten sposób:
bool func(const std::unique_ptr<BaseClass>& ptr, int other_arg);
Ale czuję się bardzo nieswojo, robiąc to, po pierwsze, ponieważ wydaje mi się, że przekazanie czegoś już wpisanego _ptr
jako odniesienia nie jest instynktowne , co byłoby odniesieniem odniesienia. Po drugie, ponieważ podpis funkcji staje się jeszcze większy. Po trzecie, ponieważ w wygenerowanym kodzie potrzebne byłyby dwa następujące po sobie pośrednie wskaźniki, aby dotrzeć do mojej zmiennej.
źródło
std::unique_ptr
zastd::vector<std::unique_ptr>
kłótnię?Zaletą używania
std::unique_ptr<T>
(poza tym, że nie trzeba pamiętać o wywołaniudelete
lubdelete[]
jawnie) jest to, że gwarantuje ono, że wskaźnik jest albonullptr
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żynullptr
sprawę, albo poprosibool func(...)
o obliczenie wyniku:if (ptr) { result = func(*ptr, some_int); } else { /* the object was, for some reason, either not created or destroyed */ }
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ń:
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 narzutshared_ptr
i nie jest możliweunique_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).
źródło
Zgadzam się z Martinho, ale myślę, że ważne jest, aby zwrócić uwagę na semantykę własności przekazu referencyjnego. Myślę, że poprawnym rozwiązaniem jest użycie tutaj prostego pass-by-reference:
bool func(BaseClass& base, int other_arg);
Powszechnie akceptowane znaczenie przekazywania przez odwołanie w C ++ jest takie, jakby wywołujący funkcję mówi funkcji „tutaj, możesz pożyczyć ten obiekt, używać go i modyfikować (jeśli nie jest stała), ale tylko dla czas trwania funkcji. " Nie jest to w żaden sposób sprzeczne z regułami własności obowiązującymi w przypadku,
unique_ptr
gdy przedmiot jest wypożyczany tylko na krótki okres czasu, nie dochodzi do faktycznego przeniesienia własności (jeśli pożyczasz komuś samochód, czy podpisujesz tytuł do niego?).Tak więc, nawet jeśli wyciągnięcie odniesienia (lub nawet surowego wskaźnika) z pliku referencyjnego (lub nawet surowego wskaźnika) może wydawać się złe (pod względem projektowania, kodowania itp.)
unique_ptr
, W rzeczywistości nie jest tak, ponieważ jest ono doskonale zgodne z regułami własności określonymi przez theunique_ptr
. Oczywiście są też inne fajne zalety, takie jak czysta składnia, brak ograniczeń tylko dla obiektów należących do aunique_ptr
, i tak dalej.źródło
Osobiście unikam wyciągania odniesienia ze wskaźnika / inteligentnego wskaźnika. Bo co się stanie, jeśli wskaźnik jest
nullptr
? Jeśli zmienisz podpis na ten:bool func(BaseClass& base, int other_arg);
Być może będziesz musiał chronić swój kod przed odwołaniami do zerowego wskaźnika:
if (the_unique_ptr) func(*the_unique_ptr, 10);
Jeśli klasa jest jedynym właścicielem wskaźnika, druga alternatywa Martinho wydaje się bardziej rozsądna:
func(the_unique_ptr.get(), 10);
Alternatywnie możesz użyć
std::shared_ptr
. Jeśli jednak jest za to odpowiedzialny jeden podmiotdelete
,std::shared_ptr
koszty ogólne nie opłaca się.źródło
std::unique_ptr
nie ma żadnych kosztów ogólnych, prawda?std::shared_ptr
narzutu.