Jak przekazać std :: unique_ptr?

85

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 _ptrjako 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.

lvella
źródło

Odpowiedzi:

99

Jeśli chcesz, aby funkcja używała punktu, przekaż do niego odniesienie. Nie ma powodu, aby wiązać funkcję tylko z jakimś inteligentnym wskaźnikiem:

bool func(BaseClass& base, int other_arg);

A na stronie wezwania użyj operator*:

func(*some_unique_ptr, 42);

Alternatywnie, jeśli baseargument może mieć wartość null, zachowaj podpis bez zmian i użyj get()funkcji składowej:

bool func(BaseClass* base, int other_arg);
func(some_unique_ptr.get(), 42);
R. Martinho Fernandes
źródło
4
To miłe, ale czy pozbyłeś się tego std::unique_ptrza std::vector<std::unique_ptr>kłótnię?
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
31

Zaletą używania std::unique_ptr<T>(poza tym, że nie trzeba pamiętać o wywołaniu deletelub delete[]jawnie) jest to, że gwarantuje ono, że wskaźnik jest albo nullptrwskazuje 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 constkwalifikatorami 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 nullptrsprawę, albo poprosi bool 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ń:

  1. Adres bloku pamięci, w którym znajduje się żądany obiekt. ( dobra )
  2. Adres 0x0, co do którego możesz być pewien, nie jest dereferencjonowalny i może mieć semantykę „nic” lub „brak obiektu”. ( zły )
  3. Adres bloku pamięci, który znajduje się poza przestrzenią adresowalną twojego procesu (wyłuskiwanie go, miejmy nadzieję, spowoduje awarię programu). ( brzydki )
  4. 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_ptri 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 nullptrparametr 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).

Victor Savu
źródło
25

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_ptrgdy 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 the unique_ptr. Oczywiście są też inne fajne zalety, takie jak czysta składnia, brak ograniczeń tylko dla obiektów należących do a unique_ptr, i tak dalej.

Mikael Persson
źródło
0

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 podmiot delete, std::shared_ptrkoszty ogólne nie opłaca się.

Guilherme Ferreira
źródło
Czy zdajesz sobie sprawę, że w większości przypadków std::unique_ptrnie ma żadnych kosztów ogólnych, prawda?
lvella
1
Tak, jestem tego świadomy. Dlatego powiedziałem mu o std::shared_ptrnarzutu.
Guilherme Ferreira