Jeśli mam funkcję, która musi współpracować z a shared_ptr
, czy nie byłoby bardziej wydajne przekazanie jej odwołania do niej (aby uniknąć kopiowania shared_ptr
obiektu)? Jakie są możliwe złe skutki uboczne? Wyobrażam sobie dwa możliwe przypadki:
1) wewnątrz funkcji tworzona jest kopia argumentu, jak w
ClassA::take_copy_of_sp(boost::shared_ptr<foo> &sp)
{
...
m_sp_member=sp; //This will copy the object, incrementing refcount
...
}
2) wewnątrz funkcji argument jest używany tylko, jak w
Class::only_work_with_sp(boost::shared_ptr<foo> &sp) //Again, no copy here
{
...
sp->do_something();
...
}
Nie widzę w obu przypadkach dobrego powodu, aby przekazać boost::shared_ptr<foo>
wartość przez zamiast przez odniesienie. Przekazywanie przez wartość tylko „tymczasowo” zwiększyłoby liczbę odwołań z powodu kopiowania, a następnie zmniejszyłoby ją przy wychodzeniu z zakresu funkcji. Czy coś przeoczę?
Dla wyjaśnienia, po przeczytaniu kilku odpowiedzi: doskonale zgadzam się z obawami przedwczesnej optymalizacji i zawsze staram się najpierw profilować, a potem pracować nad hotspotami. Moje pytanie było bardziej z czysto technicznego punktu widzenia kodu, jeśli wiesz, co mam na myśli.
źródło
Odpowiedzi:
Celem odrębnej
shared_ptr
instancji jest zagwarantowanie (w miarę możliwości), że tak długo, jakshared_ptr
jest to w zakresie, obiekt, na który wskazuje, będzie nadal istniał, ponieważ jego liczba odwołań będzie wynosić co najmniej 1.Dlatego używając odwołania do a
shared_ptr
, wyłączasz tę gwarancję. A więc w drugim przypadku:Skąd wiesz, że
sp->do_something()
nie wybuchnie z powodu zerowego wskaźnika?Wszystko zależy od tego, co znajduje się w tych sekcjach „…” kodu. A co, jeśli nazwiesz coś podczas pierwszego „...”, co ma efekt uboczny (gdzieś w innej części kodu), polegający na wyczyszczeniu a
shared_ptr
do tego samego obiektu? A co, jeśli okaże się, że jest to jedyna odrębna cechashared_ptr
tego obiektu? Żegnaj obiekt, właśnie tam, gdzie masz zamiar spróbować go użyć.Istnieją więc dwa sposoby odpowiedzi na to pytanie:
Zbadaj dokładnie źródło całego programu, aż będziesz pewien, że obiekt nie umrze podczas działania ciała funkcji.
Zmień parametr z powrotem, aby był oddzielnym obiektem zamiast odniesienia.
Ogólna rada, która ma tutaj zastosowanie: nie zawracaj sobie głowy wprowadzaniem ryzykownych zmian w kodzie ze względu na wydajność, dopóki nie ustawisz czasu produktu w realistycznej sytuacji w programie profilującym i ostatecznie nie zmierzysz, że zmiana, którą chcesz wprowadzić, spowoduje znacząca różnica w wydajności.
Aktualizacja dla komentującego JQ
Oto wymyślony przykład. Jest to celowo proste, więc błąd będzie oczywisty. W prawdziwych przykładach błąd nie jest tak oczywisty, ponieważ jest ukryty w warstwach prawdziwych szczegółów.
Mamy funkcję, która gdzieś wyśle wiadomość. Może to być duża wiadomość, więc zamiast używać znaku,
std::string
który prawdopodobnie zostanie skopiowany, gdy jest przesyłany do wielu miejsc, używamy ashared_ptr
do łańcucha:(W tym przykładzie po prostu „wysyłamy” go do konsoli).
Teraz chcemy dodać możliwość zapamiętania poprzedniej wiadomości. Chcemy następującego zachowania: musi istnieć zmienna, która zawiera ostatnio wysłaną wiadomość, ale gdy wiadomość jest aktualnie wysyłana, nie może być poprzedniej wiadomości (zmienną należy zresetować przed wysłaniem). Dlatego deklarujemy nową zmienną:
Następnie zmieniamy naszą funkcję zgodnie z określonymi przez nas zasadami:
Tak więc przed rozpoczęciem wysyłania odrzucamy bieżącą poprzednią wiadomość, a po zakończeniu wysyłania możemy zapisać nową poprzednią wiadomość. Wszystko dobrze. Oto kod testowy:
I zgodnie z oczekiwaniami drukuje się to
Hi!
dwukrotnie.Teraz pojawia się pan Maintainer, który patrzy na kod i myśli: Hej, ten parametr
send_message
toshared_ptr
:Oczywiście można to zmienić na:
Pomyśl o poprawie wydajności, jaką to przyniesie! (Nieważne, że mamy zamiar wysłać zazwyczaj dużą wiadomość na jakimś kanale, więc poprawa wydajności będzie tak mała, że będzie niemożliwa do zmierzenia).
Ale prawdziwym problemem jest to, że teraz kod testowy będzie wykazywał niezdefiniowane zachowanie (w kompilacjach debugowania Visual C ++ 2010 dochodzi do awarii).
Pan Maintainer jest tym zaskoczony, ale dodaje test obronny,
send_message
próbując powstrzymać problem:Ale oczywiście nadal działa i ulega awarii, ponieważ
msg
nigdy nie jest zerowy, gdysend_message
zostanie wywołany.Jak mówię, mając cały kod tak blisko siebie w trywialnym przykładzie, łatwo jest znaleźć błąd. Ale w rzeczywistych programach, w których występują bardziej złożone relacje między zmiennymi obiektami, które zawierają wzajemne wskaźniki, łatwo jest popełnić błąd i trudno jest skonstruować niezbędne przypadki testowe, aby wykryć błąd.
Prostym rozwiązaniem, w którym chcesz, aby funkcja mogła polegać na
shared_ptr
kontynuacji, która nie jest zerowa przez cały czas, polega na tym, że funkcja przydziela swoją własną wartość trueshared_ptr
, zamiast polegać na odwołaniu do istniejącegoshared_ptr
.Wadą jest to, że skopiowane a
shared_ptr
nie jest darmowe: nawet implementacje „bez blokad” muszą używać operacji blokowanych, aby przestrzegać gwarancji wątków. Mogą więc zaistnieć sytuacje, w których program można znacznie przyspieszyć, zmieniając plikshared_ptr
wshared_ptr &
. Ale nie jest to zmiana, którą można bezpiecznie wprowadzić we wszystkich programach. Zmienia logiczne znaczenie programu.Zauważ, że podobny błąd wystąpiłby, gdybyśmy użyli
std::string
całej zamiaststd::shared_ptr<std::string>
i zamiast:aby wyczyścić przekaz, powiedzieliśmy:
Wtedy symptomem byłoby przypadkowe wysłanie pustej wiadomości, zamiast nieokreślonego zachowania. Koszt dodatkowej kopii bardzo dużego ciągu może być dużo bardziej znaczący niż koszt skopiowania a
shared_ptr
, więc kompromis może być inny.źródło
Okazało się, że nie zgadzam się z odpowiedzią, która została najwyżej oceniona, więc poszedłem na poszukiwanie opinii ekspertów i oto one. Od http://channel9.msdn.com/Shows/Going+Deep/C-and-Beyond-2011-Scott-Andrei-and-Herb-Ask-Us-Anything
Herb Sutter: „kiedy przekazujesz shared_ptrs, kopie są drogie”
Scott Meyers: „Nie ma nic specjalnego w shared_ptr, jeśli chodzi o to, czy przekazujesz to przez wartość, czy przez referencję. Użyj dokładnie tej samej analizy, której używasz dla każdego innego typu zdefiniowanego przez użytkownika. Wydaje się, że ludzie mają takie przekonanie, że shared_ptr w jakiś sposób rozwiązuje wszystkie problemy związane z zarządzaniem, a ponieważ jest mały, przekazanie wartości z konieczności jest niedrogie. Musi zostać skopiowane, a wiąże się to z kosztami ... przypisanie wartości jest drogie, więc jeśli ujdzie mi to na sucho z odpowiednią semantyką w moim programie, przekażę to przez odniesienie do const lub referencji "
Herb Sutter: "zawsze przekazuj je przez odniesienie do const, a bardzo czasami może dlatego, że wiesz, że to, co wywołałeś, może zmodyfikować rzecz, z której masz odniesienie, może wtedy możesz przekazać wartość ... jeśli skopiujesz je jako parametry, och Mój Boże, prawie nigdy nie musisz zwiększać liczby referencji, ponieważ i tak jest ona utrzymywana przy życiu, a powinieneś przekazywać ją przez referencje, więc zrób to "
Aktualizacja: Herb rozwinął to tutaj: http://herbsutter.com/2013/06/05/gotw-91-solution-smart-pointer-parameters/ , chociaż morał tej historii jest taki, że nie powinieneś przechodzić shared_ptrs w ogóle „chyba że chcesz używać inteligentnego wskaźnika lub manipulować nim, na przykład udostępniać lub przenosić własność”.
źródło
shared_ptr
s, nie było zgody co do kwestii przekazywania wartości w porównaniu z referencją.const &
wpłynie na semantykę, jest bardzo łatwe proste programy.Odradzałbym tę praktykę, chyba że ty i inni programiści, z którymi pracujesz, naprawdę naprawdę wiecie, co wszyscy robicie.
Po pierwsze, nie masz pojęcia, w jaki sposób interfejs Twojej klasy może ewoluować i chcesz uniemożliwić innym programistom robienie złych rzeczy. Przekazywanie shared_ptr przez odniesienie nie jest czymś, czego programista powinien się spodziewać, ponieważ nie jest to idiomatyczne, a to ułatwia jego niepoprawne użycie. Programuj defensywnie: utrudnij niepoprawne korzystanie z interfejsu. Przekazywanie przez odniesienie spowoduje tylko późniejsze problemy.
Po drugie, nie optymalizuj, dopóki nie wiesz, że ta konkretna klasa będzie problemem. Najpierw profil, a potem, jeśli twój program naprawdę potrzebuje wzmocnienia, jakie daje przekazywanie przez odniesienie, to może. W przeciwnym razie nie przejmuj się drobiazgami (np. Dodatkowymi instrukcjami N potrzebnymi do przekazania wartości), zamiast tego martw się o projekt, struktury danych, algorytmy i długoterminową konserwację.
źródło
Tak, wzięcie referencji jest w porządku. Nie zamierzasz udostępniać metody współwłasności; chce tylko z tym pracować. Możesz wziąć referencje również dla pierwszego przypadku, ponieważ i tak je kopiujesz. Ale w pierwszym przypadku przejmuje własność. Jest taka sztuczka, aby skopiować ją tylko raz:
Powinieneś także skopiować, kiedy go zwracasz (tj. Nie zwracać odniesienia). Ponieważ twoja klasa nie wie, co robi z nią klient (może przechowywać do niej wskaźnik, a wtedy nastąpi wielki wybuch). Jeśli później okaże się, że to wąskie gardło (pierwszy profil!), Nadal możesz zwrócić odwołanie.
Edycja : Oczywiście, jak zauważają inni, jest to prawdą tylko wtedy, gdy znasz swój kod i wiesz, że w jakiś sposób nie resetujesz przekazanego współdzielonego wskaźnika. W razie wątpliwości podaj wartość.
źródło
Rozsądnie jest przejść
shared_ptr
obokconst&
. Prawdopodobnie nie spowoduje to problemów (z wyjątkiem mało prawdopodobnego przypadku, gdy odwołanieshared_ptr
zostanie usunięte podczas wywołania funkcji, jak opisano szczegółowo w Earwicker) i prawdopodobnie będzie szybsze, jeśli przekażesz ich dużo. Zapamiętaj; wartość domyślnaboost::shared_ptr
jest bezpieczna dla wątków, więc kopiowanie obejmuje bezpieczny dla wątków przyrost.Spróbuj użyć,
const&
a nie tylko&
, ponieważ obiekty tymczasowe mogą nie być przekazywane przez odwołanie inne niż stałe. (Mimo że rozszerzenie języka w MSVC pozwala to zrobić)źródło
W drugim przypadku zrobienie tego jest prostsze:
Możesz to nazwać
źródło
Unikałbym „zwykłego” odwołania, chyba że funkcja wyraźnie modyfikuje wskaźnik.
A
const &
może być rozsądną mikrooptymalizacją przy wywoływaniu małych funkcji - np. W celu umożliwienia dalszych optymalizacji, takich jak wstawianie niektórych warunków. Ponadto przyrost / spadek - ponieważ jest bezpieczny dla wątków - jest punktem synchronizacji. Nie spodziewałbym się jednak, że zrobi to dużą różnicę w większości scenariuszy.Ogólnie rzecz biorąc, powinieneś używać prostszego stylu, chyba że masz powód, aby tego nie robić. Następnie używaj
const &
konsekwentnie lub dodaj komentarz, dlaczego używasz go tylko w kilku miejscach.źródło
Zalecałbym przekazywanie współdzielonego wskaźnika przez odwołanie do stałej - semantyka, że funkcja przekazywana ze wskaźnikiem NIE jest właścicielem wskaźnika, co jest czystym idiomem dla programistów.
Jedyną pułapką jest to, że w programach wielowątkowych obiekt wskazywany przez wspólny wskaźnik zostaje zniszczony w innym wątku. Dlatego można śmiało powiedzieć, że używanie stałej referencji współdzielonego wskaźnika jest bezpieczne w programie jednowątkowym.
Przekazywanie współdzielonego wskaźnika przez odniesienie inne niż const jest czasami niebezpieczne - przyczyną jest funkcja swap i reset, którą funkcja może wywołać w środku, aby zniszczyć obiekt, który nadal jest uważany za ważny po powrocie funkcji.
Wydaje mi się, że nie chodzi o przedwczesną optymalizację - chodzi o unikanie niepotrzebnego marnowania cykli procesora, gdy jest jasne, co chcesz zrobić, a idiom kodowania został mocno przyjęty przez innych programistów.
Tylko moje 2 centy :-)
źródło
Wygląda na to, że wszystkie zalety i wady tutaj można uogólnić na DOWOLNY typ przekazany przez odwołanie, a nie tylko shared_ptr. Moim zdaniem powinieneś znać semantykę przekazywania przez referencję, stałą referencję i wartość i używać jej poprawnie. Ale nie ma absolutnie nic złego w przekazywaniu shared_ptr przez odniesienie, chyba że uważasz, że wszystkie odwołania są złe ...
Wróćmy do przykładu:
Skąd wiesz, że
sp.do_something()
nie wybuchnie z powodu zwisającej wskazówki?Prawda jest taka, że, shared_ptr lub nie, const lub nie, może się to zdarzyć, jeśli masz wadę projektową, na przykład bezpośrednie lub pośrednie dzielenie własności
sp
między wątkami, nieużywanie obiektu, który to robidelete this
, masz okrągłą własność lub inne błędy własności.źródło
Jedną rzeczą, o której jeszcze nie widziałem, jest to, że kiedy przekazujesz wskaźniki współdzielone przez odniesienie, tracisz niejawną konwersję, którą otrzymujesz, jeśli chcesz przekazać wskaźnik współdzielonej klasy pochodnej przez odniesienie do wskaźnika współdzielonego klasy bazowej.
Na przykład ten kod spowoduje błąd, ale zadziała, jeśli zmienisz
test()
tak, że wskaźnik współdzielony nie jest przekazywany przez odwołanie.źródło
Zakładam, że znasz przedwczesną optymalizację i pytasz o to albo w celach akademickich, albo dlatego, że wyizolowałeś wcześniej istniejący kod, który nie działa.
Przekazywanie przez odniesienie jest w porządku
Przekazywanie przez odwołanie do stałej jest lepsze i zwykle może być używane, ponieważ nie wymusza trwałości na wskazanym obiekcie.
Jesteś nie grozi utrata wskaźnik spowodowane użyciem referencji. To odniesienie jest dowodem na to, że masz kopię inteligentnego wskaźnika wcześniej na stosie i tylko jeden wątek jest właścicielem stosu wywołań, więc wcześniej istniejąca kopia nie zniknie.
Korzystanie z odniesień jest często bardziej wydajne z powodów, o których wspominasz, ale nie jest gwarantowane . Pamiętaj, że wyłuskiwanie obiektów również może wymagać pracy. Idealnym scenariuszem użycia referencji byłoby, gdyby twój styl kodowania obejmował wiele małych funkcji, w których wskaźnik byłby przekazywany z funkcji do funkcji do funkcji przed użyciem.
Należy zawsze unikać przechowywania inteligentnego wskaźnika jako odniesienia. Twój
Class::take_copy_of_sp(&sp)
przykład pokazuje prawidłowe użycie.źródło
Zakładając, że nie interesuje nas poprawność const (lub więcej, masz na myśli zezwolenie funkcjom na modyfikowanie lub współdzielenie własności przekazywanych danych), przekazanie boost :: shared_ptr przez wartość jest bezpieczniejsze niż przekazanie go przez odniesienie, ponieważ pozwalamy oryginalnemu boost :: shared_ptr na kontrolowanie własnego czasu życia. Rozważ wyniki następującego kodu ...
Z powyższego przykładu widzimy, że przekazanie sharedA przez odniesienie pozwala FooTakesReference na zresetowanie oryginalnego wskaźnika, co zmniejsza jego liczbę do 0, niszcząc jego dane. Jednak FooTakesValue nie może zresetować oryginalnego wskaźnika, gwarantując, że dane SharedB są nadal użyteczne. Kiedy nieuchronnie pojawia się inny programista i próbuje na barana wykorzystać kruche istnienie sharedA , następuje chaos. Jednak szczęśliwy programista sharedB wraca do domu wcześnie, ponieważ w jego świecie wszystko jest w porządku.
W tym przypadku bezpieczeństwo kodu znacznie przewyższa jakąkolwiek poprawę szybkości kopiowania. Jednocześnie boost :: shared_ptr ma na celu poprawę bezpieczeństwa kodu. Dużo łatwiej będzie przejść od kopii do referencji, jeśli coś wymaga takiej niszowej optymalizacji.
źródło
Sandy napisał: „Wygląda na to, że wszystkie za i przeciw tutaj można uogólnić na DOWOLNY typ przekazany przez odwołanie, a nie tylko shared_ptr”.
Do pewnego stopnia to prawda, ale celem korzystania z shared_ptr jest wyeliminowanie obaw dotyczących czasu życia obiektów i pozwolenie kompilatorowi zająć się tym za Ciebie. Jeśli zamierzasz przekazać udostępniony wskaźnik przez referencję i zezwolić klientom na wywołanie metod innych niż stałe wywołania zliczanych obiektów referencyjnych, które mogą uwolnić dane obiektu, to użycie wskaźnika współdzielonego jest prawie bezcelowe.
Napisałem „prawie” w tym poprzednim zdaniu, ponieważ wydajność może budzić obawy i może to być uzasadnione w rzadkich przypadkach, ale ja też unikałbym tego scenariusza i sam szukał wszystkich możliwych innych rozwiązań optymalizacyjnych, takich jak na poważnie przy dodawaniu kolejnego poziomu pośrednictwa, leniwej oceny itp.
Kod, który istnieje poza autorem, a nawet publikuje w pamięci autora, który wymaga niejawnych założeń dotyczących zachowania, w szczególności zachowania dotyczącego życia obiektów, wymaga jasnej, zwięzłej, czytelnej dokumentacji, a wielu klientów i tak go nie przeczyta! Prostota prawie zawsze przeważa nad wydajnością i prawie zawsze istnieją inne sposoby na zwiększenie wydajności. Jeśli naprawdę potrzebujesz przekazać wartości przez odniesienie, aby uniknąć głębokiego kopiowania przez konstruktory kopiujące twoich obiektów zliczanych referencji (i operatora równości), być może powinieneś rozważyć sposoby, aby głęboko skopiowane dane były liczonymi wskaźnikami odniesienia, które mogą być skopiowane szybko. (Oczywiście to tylko jeden scenariusz projektowy, który może nie mieć zastosowania w Twojej sytuacji).
źródło
Kiedyś pracowałem w projekcie, w którym zasada była bardzo silna, jeśli chodzi o przekazywanie inteligentnych wskaźników według wartości. Kiedy poproszono mnie o przeprowadzenie analizy wydajności - stwierdziłem, że na zwiększanie i zmniejszanie liczników odniesienia inteligentnych wskaźników aplikacja zużywa od 4 do 6% wykorzystanego czasu procesora.
Jeśli chcesz przekazać inteligentne wskaźniki według wartości, aby uniknąć problemów w dziwnych przypadkach opisanych przez Daniela Earwickera, upewnij się, że rozumiesz cenę, jaką za to płacisz.
Jeśli zdecydujesz się przejść z referencją, głównym powodem użycia odwołania const jest umożliwienie niejawnego upcastingu, gdy musisz przekazać wspólny wskaźnik do obiektu z klasy, która dziedziczy klasę, której używasz w interfejsie.
źródło
Oprócz tego, co powiedział litb, chciałbym zauważyć, że prawdopodobnie jest to przekazywane przez odwołanie do const w drugim przykładzie, dzięki czemu masz pewność, że nie zmodyfikujesz go przypadkowo.
źródło
przekazać wartość:
przekazać przez odniesienie:
pass by pointer:
źródło
Każdy fragment kodu musi mieć jakiś sens. Jeśli przekażesz wspólny wskaźnik według wartości wszędzie w aplikacji, oznacza to „Nie jestem pewien, co się dzieje gdzie indziej, dlatego preferuję surowe bezpieczeństwo ”. To nie jest to, co nazywam dobrym znakiem zaufania dla innych programistów, którzy mogliby zapoznać się z kodem.
W każdym razie, nawet jeśli funkcja otrzyma odwołanie do stałej i nie jesteś pewien, nadal możesz utworzyć kopię współdzielonego wskaźnika na początku funkcji, aby dodać mocne odniesienie do wskaźnika. Można to również potraktować jako wskazówkę dotyczącą projektu („wskaźnik można zmodyfikować w innym miejscu”).
Więc tak, IMO, domyślną wartością powinno być „ pass by const reference ”.
źródło