C ++ - przekazywanie referencji do std :: shared_ptr lub boost :: shared_ptr


115

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_ptrobiektu)? 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.


Nie wiem, czy możesz zmodyfikować tagi swojego pytania, ale spróbuj dodać tam tag przyspieszenia. Próbowałem znaleźć to pytanie, ale nie mogłem znaleźć żadnego, ponieważ szukałem tagów doładowania i inteligentnego wskaźnika. Więc znalazłem twoje pytanie tuż po utworzeniu własnego pytania
Edison Gustavo Muenz

Odpowiedzi:


113

Celem odrębnej shared_ptrinstancji jest zagwarantowanie (w miarę możliwości), że tak długo, jak shared_ptrjest to w zakresie, obiekt, na który wskazuje, będzie nadal istniał, ponieważ jego liczba odwołań będzie wynosić co najmniej 1.

Class::only_work_with_sp(boost::shared_ptr<foo> sp)
{
    // sp points to an object that cannot be destroyed during this function
}

Dlatego używając odwołania do a shared_ptr, wyłączasz tę gwarancję. A więc w drugim przypadku:

Class::only_work_with_sp(boost::shared_ptr<foo> &sp) //Again, no copy here  
{    
    ...  
    sp->do_something();  
    ...  
}

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_ptrdo tego samego obiektu? A co, jeśli okaże się, że jest to jedyna odrębna cecha shared_ptrtego obiektu? Żegnaj obiekt, właśnie tam, gdzie masz zamiar spróbować go użyć.

Istnieją więc dwa sposoby odpowiedzi na to pytanie:

  1. Zbadaj dokładnie źródło całego programu, aż będziesz pewien, że obiekt nie umrze podczas działania ciała funkcji.

  2. 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::stringktóry prawdopodobnie zostanie skopiowany, gdy jest przesyłany do wielu miejsc, używamy a shared_ptrdo łańcucha:

void send_message(std::shared_ptr<std::string> msg)
{
    std::cout << (*msg.get()) << std::endl;
}

(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ą:

std::shared_ptr<std::string> previous_message;

Następnie zmieniamy naszą funkcję zgodnie z określonymi przez nas zasadami:

void send_message(std::shared_ptr<std::string> msg)
{
    previous_message = 0;
    std::cout << *msg << std::endl;
    previous_message = msg;
}

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:

send_message(std::shared_ptr<std::string>(new std::string("Hi")));
send_message(previous_message);

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_messageto shared_ptr:

void send_message(std::shared_ptr<std::string> msg)

Oczywiście można to zmienić na:

void send_message(const std::shared_ptr<std::string> &msg)

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_messagepróbując powstrzymać problem:

void send_message(const std::shared_ptr<std::string> &msg)
{
    if (msg == 0)
        return;

Ale oczywiście nadal działa i ulega awarii, ponieważ msgnigdy nie jest zerowy, gdy send_messagezostanie 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_ptrkontynuacji, która nie jest zerowa przez cały czas, polega na tym, że funkcja przydziela swoją własną wartość true shared_ptr, zamiast polegać na odwołaniu do istniejącego shared_ptr.

Wadą jest to, że skopiowane a shared_ptrnie 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 plik shared_ptrw shared_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::stringcałej zamiast std::shared_ptr<std::string>i zamiast:

previous_message = 0;

aby wyczyścić przekaz, powiedzieliśmy:

previous_message.clear();

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.


16
Przekazany shared_ptr już istnieje w zakresie w witrynie wywołania. Możesz stworzyć skomplikowany scenariusz, w którym kod w tym pytaniu wysadziłby się z powodu wiszącego wskaźnika, ale przypuszczam, że masz większe problemy niż parametr referencyjny!
Magnus Hoff

10
Może być przechowywany w członku. Możesz nazwać coś, co zdarzy się oczyścić tego członka. Celem smart_ptr jest uniknięcie konieczności koordynowania czasów życia w hierarchiach lub zakresach, które zagnieżdżają się wokół stosu wywołań, dlatego najlepiej jest założyć, że w takich programach życia tego nie robią.
Daniel Earwicker

7
Tak naprawdę nie jest to jednak mój punkt widzenia! Jeśli uważasz, że to, co mówię, ma związek z moim kodem, być może mnie nie zrozumiałeś. Mówię o nieuniknionej implikacji przyczyny, dla której shared_ptr istnieje w pierwszej kolejności: wiele okresów istnienia obiektów nie jest po prostu związanych z wywołaniami funkcji.
Daniel Earwicker

8
@DanielEarwicker całkowicie zgadza się ze wszystkimi Twoimi uwagami i jestem zaskoczony poziomem sprzeciwu. Coś, co sprawia, że ​​twoje obawy są jeszcze bardziej istotne, to wątkowanie, kiedy to się pojawia, gwarancje dotyczące poprawności obiektów stają się znacznie ważniejsze. Dobra odpowiedź.
radman

3
Niedawno ścigałem bardzo poważny błąd, który był spowodowany przekazaniem odniesienia do współdzielonego wskaźnika. Kod obsługiwał zmianę stanu obiektu i gdy zauważył zmianę stanu obiektu, usuwał go z kolekcji obiektów w poprzednim stanie i przenosił do kolekcji obiektów w nowym stanie. Operacja usuwania zniszczyła ostatni udostępniony wskaźnik do obiektu. Funkcja członkowska została wywołana w odniesieniu do współdzielonego wskaźnika w kolekcji. Bum. Daniel Earwicker ma rację.
David Schwartz,

115

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ść”.


8
Niezłe znalezisko! Wspaniale jest widzieć dwóch czołowych ekspertów w tej dziedzinie, którzy publicznie obalają konwencjonalną opinię na temat SO.
Stephan Tolksdorf

3
„Nie ma nic specjalnego w shared_ptr, jeśli chodzi o to, czy przekazujesz to przez wartość, czy przez odniesienie” - naprawdę nie zgadzam się z tym. To jest wyjątkowe. Osobiście wolałbym grać ostrożnie i przyjąć niewielki hit wydajnościowy. Jeśli istnieje określony obszar kodu, który muszę zoptymalizować, to na pewno, przyjrzałbym się korzyściom wydajnościowym z przekazaniem shared_ptr przez const ref.
JasonZ

3
Warto również zauważyć, że chociaż istniała zgoda co do nadużywania shared_ptrs, nie było zgody co do kwestii przekazywania wartości w porównaniu z referencją.
Nicol Bolas

2
Scott Meyers: „więc jeśli ujdzie mi to na sucho z odpowiednią semantyką w moim programie…” tj. Nie zaprzecza w ogóle mojej odpowiedzi, co wskazuje, że ustalenie, czy zmiana parametrów na const &wpłynie na semantykę, jest bardzo łatwe proste programy.
Daniel Earwicker

7
Herb Sutter: „bardzo czasami może dlatego, że wiesz, że to, co nazwałeś, może zmienić to, z czego masz odniesienie”. Ponownie, to małe zwolnienie dla drobnej sprawy, więc nie zaprzecza mojej odpowiedzi. Pozostaje pytanie: skąd wiesz, że używanie const ref jest bezpieczne? Bardzo łatwo udowodnić w prostym programie, nie tak łatwo w złożonym programie. Ale hej, to jest C ++, więc preferujemy przedwczesną mikro-optymalizację ponad prawie wszystkie inne problemy inżynieryjne, prawda ?! :)
Daniel Earwicker

22

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


Chociaż odpowiedź Litba jest technicznie poprawna, nigdy nie lekceważ „lenistwa” programistów (ja też jestem leniwy!). littlenag odpowiada lepiej, że odniesienie do shared_ptr będzie nieoczekiwane i prawdopodobnie (prawdopodobnie) niepotrzebna optymalizacja, która utrudni przyszłą konserwację.
netjeff

18

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:

void ClassA::take_copy_of_sp(boost::shared_ptr<foo> sp) {
    m_sp_member.swap(sp);
}

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ść.


11

Rozsądnie jest przejść shared_ptrobok const&. Prawdopodobnie nie spowoduje to problemów (z wyjątkiem mało prawdopodobnego przypadku, gdy odwołanie shared_ptrzostanie 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ślna boost::shared_ptrjest 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ć)


3
Tak, zawsze używam referencji const, po prostu zapomniałem umieścić to w moim przykładzie. W każdym razie MSVC pozwala na wiązanie odwołań innych niż const do tymczasowych nie z powodu błędu, ale dlatego, że domyślnie ma właściwość „C / C ++ -> Język -> Wyłącz rozszerzenie języka” ustawioną na „NIE”. Włącz to, a nie skompiluje ich ...
abigagli

abigagli: Poważnie? Słodkie! Będę egzekwował to w pracy, jutro pierwsza rzecz;)
Magnus Hoff

10

W drugim przypadku zrobienie tego jest prostsze:

Class::only_work_with_sp(foo &sp)
{    
    ...  
    sp.do_something();  
    ...  
}

Możesz to nazwać

only_work_with_sp(*sp);

3
Jeśli przyjmiesz konwencję używania odniesień do obiektów, gdy nie musisz robić kopii wskaźnika, służy to również do udokumentowania twoich zamiarów. Daje ci również szansę na użycie odwołania const.
Mark Ransom

Tak, zgadzam się na użycie odniesień do obiektów jako środka do wyrażenia, że ​​wywoływana funkcja nie „pamięta” niczego o tym obiekcie. Zwykle używam argumentów formalnych wskaźnika, jeśli funkcja „śledzi” obiekt
abigagli

3

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.


3

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 :-)


1
Zobacz komentarz powyżej Davida Schwartza "... ścigałem bardzo poważny błąd, który był spowodowany przekazaniem odniesienia do współdzielonego wskaźnika. Kod obsługiwał zmianę stanu obiektu i kiedy zauważył, że stan obiektu się zmienił, usunął go z kolekcji obiektów w poprzednim stanie i przeniósł do kolekcji obiektów w nowym stanie. Operacja usuwania zniszczyła ostatni wspólny wskaźnik do obiektu. Funkcja członkowska została wywołana w odniesieniu do wskaźnika udostępnionego w kolekcji. Boom… ”
Jason Harrison,

3

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:

Class::only_work_with_sp( foo &sp ) //Again, no copy here  
{    
    ...  
    sp.do_something();  
    ...  
}

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 spmiędzy wątkami, nieużywanie obiektu, który to robi delete this, masz okrągłą własność lub inne błędy własności.


2

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.

#include <boost/shared_ptr.hpp>

class Base { };
class Derived: public Base { };

// ONLY instances of Base can be passed by reference.  If you have a shared_ptr
// to a derived type, you have to cast it manually.  If you remove the reference
// and pass the shared_ptr by value, then the cast is implicit so you don't have
// to worry about it.
void test(boost::shared_ptr<Base>& b)
{
    return;
}

int main(void)
{
    boost::shared_ptr<Derived> d(new Derived);
    test(d);

    // If you want the above call to work with references, you will have to manually cast
    // pointers like this, EVERY time you call the function.  Since you are creating a new
    // shared pointer, you lose the benefit of passing by reference.
    boost::shared_ptr<Base> b = boost::dynamic_pointer_cast<Base>(d);
    test(b);

    return 0;
}

1

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.


1
„Nie grozi Ci utrata wskaźnika z powodu użycia referencji. To odniesienie jest dowodem na to, że masz kopię inteligentnego wskaźnika znajdującą się wcześniej w stosie” Albo członek danych ...?
Daniel Earwicker,

Rozważ magię boost :: thread i boost :: ref: boost :: function <int> functionPointer = boost :: bind (doSomething, boost :: ref (sharedPtrInstance)); m_workerThread = new boost :: thread (functionPointer); ... usuń sharedPtrInstance
Jason Harrison

1

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

void FooTakesReference( boost::shared_ptr< int > & ptr )
{
    ptr.reset(); // We reset, and so does sharedA, memory is deleted.
}

void FooTakesValue( boost::shared_ptr< int > ptr )
{
    ptr.reset(); // Our temporary is reset, however sharedB hasn't.
}

void main()
{
    boost::shared_ptr< int > sharedA( new int( 13 ) );
    boost::shared_ptr< int > sharedB( new int( 14 ) );

    FooTakesReference( sharedA );

    FooTakesValue( sharedB );
}

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.


1

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


1

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.


0

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.


0
struct A {
  shared_ptr<Message> msg;
  shared_ptr<Message> * ptr_msg;
}
  1. przekazać wartość:

    void set(shared_ptr<Message> msg) {
      this->msg = msg; /// create a new shared_ptr, reference count will be added;
    } /// out of method, new created shared_ptr will be deleted, of course, reference count also be reduced;
  2. przekazać przez odniesienie:

    void set(shared_ptr<Message>& msg) {
     this->msg = msg; /// reference count will be added, because reference is just an alias.
     }
  3. pass by pointer:

    void set(shared_ptr<Message>* msg) {
      this->ptr_msg = msg; /// reference count will not be added;
    }

0

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

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.