Celem odrębnej shared_ptr
instancji jest zagwarantowanie (w miarę możliwości), że tak długo, jak shared_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.
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_ptr
do tego samego obiektu? A co, jeśli okaże się, że jest to jedyna odrębna cecha shared_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 a shared_ptr
do ł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_message
to 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_message
pró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ż msg
nigdy nie jest zerowy, gdy send_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ść true shared_ptr
, zamiast polegać na odwołaniu do istniejącego shared_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 plik shared_ptr
w 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::string
cał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.