Przekazywanie wspólnych wskaźników jako argumentów

88

Jeśli zadeklaruję obiekt zawinięty we współdzielony wskaźnik:

std::shared_ptr<myClass> myClassObject(new myClass());

następnie chciałem przekazać to jako argument do metody:

DoSomething(myClassObject);

//the called method
void DoSomething(std::shared_ptr<myClass> arg1)
{
   arg1->someField = 4;
}

Czy powyższe po prostu zwiększa liczbę referencji shared_pt i wszystko jest w porządku? A może pozostawia zwisający wskaźnik?

Nadal masz to zrobić ?:

DoSomething(myClassObject.Get());

void DoSomething(std::shared_ptr<myClass>* arg1)
{
   (*arg1)->someField = 4;
}

Myślę, że drugi sposób może być bardziej wydajny, ponieważ musi skopiować tylko 1 adres (w przeciwieństwie do całego inteligentnego wskaźnika), ale pierwszy sposób wydaje się bardziej czytelny i nie przewiduję przesuwania granic wydajności. Chcę się tylko upewnić, że nie ma w tym nic niebezpiecznego.

Dziękuję Ci.

Steve H.
źródło
14
const std::shared_ptr<myClass>& arg1
Kapitan Obvlious
3
Drugi sposób jest zepsuty, pierwszy jest idiomatyczny, jeśli faktycznie potrzebujesz swojej funkcji do współdzielenia własności. Ale czy DoSomethingnaprawdę trzeba dzielić się własnością? Wygląda na to, że zamiast tego należy użyć odniesienia ...
ildjarn
8
@SteveH: Nie, ale dlaczego twoja funkcja miałaby wymuszać specjalną semantykę własności obiektu na swoich obiektach wywołujących, jeśli w rzeczywistości ich nie potrzebuje? Twoja funkcja nie robi nic, co nie byłoby lepsze niż void DoSomething(myClass& arg1).
ildjarn
2
@SteveH: Celem inteligentnych wskaźników jest rozwiązanie nietypowych problemów związanych z własnością obiektów - jeśli ich nie masz, nie powinieneś przede wszystkim używać inteligentnych wskaźników. ; -] A shared_ptr<>konkretnie, musisz przekazać wartość, aby faktycznie być współwłasnością.
ildjarn
4
Niepowiązane: generalnie std::make_sharedjest nie tylko bardziej wydajne, ale także bezpieczniejsze niż std::shared_ptrkonstruktor.
R. Martinho Fernandes

Odpowiedzi:

171

Chcę przekazać wspólny wskaźnik do funkcji. Czy możesz mi z tym pomóc?

Jasne, mogę ci w tym pomóc. Zakładam, że rozumiesz semantykę własności w C ++. Czy to prawda?

Tak, ten temat jest mi dość wygodny.

Dobry.

Ok, przychodzą mi do głowy tylko dwa powody, dla których warto się shared_ptrkłócić:

  1. Funkcja chce współdzielić własność obiektu;
  2. Funkcja wykonuje pewną operację, która działa specjalnie na shared_ptrs.

Który Cię interesuje?

Szukam ogólnej odpowiedzi, więc właściwie jestem zainteresowany obydwoma. Jestem jednak ciekawy, co masz na myśli w przypadku nr 2.

Przykładami takich funkcji są std::static_pointer_castkomparatory niestandardowe lub predykaty. Na przykład, jeśli chcesz znaleźć wszystkie unikalne shared_ptr z wektora, potrzebujesz takiego predykatu.

Ach, kiedy funkcja faktycznie musi manipulować samym inteligentnym wskaźnikiem.

Dokładnie.

W takim przypadku uważam, że powinniśmy przejść przez odniesienie.

Tak. A jeśli to nie zmieni wskaźnika, chcesz przejść przez odwołanie do stałej. Nie ma potrzeby kopiowania, ponieważ nie musisz dzielić się własnością. To jest inny scenariusz.

Ok, rozumiem. Porozmawiajmy o innym scenariuszu.

Ten, w którym jesteś współwłasnością? Ok. W jaki sposób dzielisz się własnością shared_ptr?

Kopiując to.

Wtedy funkcja będzie musiała wykonać kopię shared_ptr, prawda?

Oczywiście. Więc przekazuję to przez odniesienie do stałej i kopiuję do zmiennej lokalnej?

Nie, to pesymizacja. Jeśli zostanie przekazany przez odniesienie, funkcja nie będzie miała innego wyboru, jak tylko wykonać kopię ręcznie. Jeśli zostanie przekazana przez wartość, kompilator wybierze najlepszy wybór między kopiowaniem a przeniesieniem i wykona go automatycznie. Więc podaj wartość.

Słuszna uwaga. Muszę częściej pamiętać ten artykuł „ Chcesz szybkości? Przekaż wartość ”.

Czekaj, co jeśli funkcja na przykład przechowuje shared_ptrzmienną składową? Czy to nie spowoduje zbędnej kopii?

Funkcja może po prostu przenieść shared_ptrargument do swojej pamięci. Przeniesienie a shared_ptrjest tanie, ponieważ nie zmienia liczby odniesień.

Ach, dobry pomysł.

Ale myślę o trzecim scenariuszu: co jeśli nie chcesz manipulować shared_ptrani dzielić się własnością?

W takim przypadku shared_ptrjest całkowicie bez znaczenia dla funkcji. Jeśli chcesz manipulować wskazanym, weź go i pozwól dzwoniącym wybrać, jakiej semantyki własności chcą.

I czy powinienem traktować wskazanego przez odniesienie czy według wartości?

Obowiązują standardowe zasady. Inteligentne wskazówki niczego nie zmieniają.

Przekaż wartość, jeśli mam zamiar kopiować, przekaż przez odniesienie, jeśli chcę uniknąć kopiowania.

Dobrze.

Hmm. Myślę, że zapomniałeś o innym scenariuszu. A jeśli chcę dzielić się własnością, ale tylko pod pewnymi warunkami?

Ach, ciekawy skrajny przypadek. Nie spodziewam się, że zdarzy się to często. Ale kiedy to się stanie, możesz albo przekazać wartość i zignorować kopię, jeśli jej nie potrzebujesz, albo przekazać przez odniesienie i utworzyć kopię, jeśli jej potrzebujesz.

Ryzykuję jedną zbędną kopię w pierwszej opcji i tracę potencjalny ruch w drugiej. Nie mogę zjeść ciasta i też go mieć?

Jeśli jesteś w sytuacji, w której to naprawdę ma znaczenie, możesz zapewnić dwa przeciążenia, jedno pobierające odwołanie do stałej lwartości, a drugie pobierające odwołanie do wartości r. Jedna kopia, druga się porusza. Szablon funkcji doskonałego przekazywania to kolejna opcja.

Myślę, że obejmuje to wszystkie możliwe scenariusze. Dziękuję Ci bardzo.

R. Martinho Fernandes
źródło
2
@Jon: Po co? To wszystko jest o zrobić A czy należy zrobić B . Myślę, że wszyscy wiemy, jak przekazywać obiekty według wartości / odniesienia, nie?
sbi
9
Ponieważ proza ​​typu „Czy to oznacza, że ​​przekażę to przez odniesienie do const, aby wykonać kopię? Nie, to pesymizacja” jest myląca dla początkującego. Przekazanie odniesienia do const nie tworzy kopii.
Jon
1
@Martinho Nie zgadzam się z zasadą „Jeśli chcesz manipulować pointee, weź go”. Jak stwierdzono w tej odpowiedzi, nieobjęcie tymczasowej własności obiektu może być niebezpieczne. Moim zdaniem domyślnie powinno być przekazanie kopii na tymczasowe prawo własności, aby zagwarantować ważność obiektu na czas wywołania funkcji.
radman
@radman Żaden scenariusz w odpowiedzi, do której odsyłasz, nie stosuje tej reguły, więc jest ona całkowicie nieistotna. Proszę napisz mi SSCCE, w którym przekazujesz przez referencję z shared_ptr<T> ptr;(tj. Wywołano void f(T&)z f(*ptr)), a obiekt nie przeżywa połączenia. Ewentualnie napisz taki, w którym omijasz wartości void f(T)i napotkane problemy.
R. Martinho Fernandes
@Martinho sprawdź mój przykładowy kod dla prostego, samodzielnego, poprawnego przykładu. Przykład dotyczy void f(T&)i dotyczy wątków. Myślę, że moim problemem jest to, że ton Twojej odpowiedzi zniechęca do przekazywania własności shared_ptr, co jest najbezpieczniejszym, najbardziej intuicyjnym i najmniej skomplikowanym sposobem ich używania. Byłbym zdecydowanie przeciwny każdemu początkującemu, aby wyodrębnił odniesienie do danych należących do shared_ptr <>. Możliwości niewłaściwego użycia są ogromne, a korzyści minimalne.
radman
22

Myślę, że ludzie niepotrzebnie boją się używania surowych wskaźników jako parametrów funkcji. Jeśli funkcja nie będzie przechowywać wskaźnika lub w inny sposób wpływać na jego żywotność, surowy wskaźnik działa równie dobrze i reprezentuje najniższy wspólny mianownik. Zastanów się na przykład, jak przekazać a unique_ptrdo funkcji, która przyjmuje shared_ptrparametr a jako parametr, według wartości lub przez odwołanie do stałej?

void DoSomething(myClass * p);

DoSomething(myClass_shared_ptr.get());
DoSomething(myClass_unique_ptr.get());

Surowy wskaźnik jako parametr funkcji nie zapobiega używaniu inteligentnych wskaźników w kodzie wywołującym, gdzie jest to naprawdę ważne.

Mark Okup
źródło
8
Jeśli używasz wskaźnika, dlaczego nie użyć po prostu odwołania? DoSomething(*a_smart_ptr)
Xeo
5
@Xeo, masz rację - to byłoby jeszcze lepsze. Czasami jednak musisz uwzględnić możliwość wskaźnika NULL.
Mark Okup
1
Jeśli masz konwencję używania surowych wskaźników jako parametrów wyjściowych, łatwiej jest zauważyć w wywołaniu kodu, że zmienna może się zmienić, np. Porównać fun(&x)do fun(x). W tym drugim przykładzie xmoże być przekazana przez wartość lub jako stała ref. Jak wspomniano powyżej, pozwoli ci to również przejść, nullptrjeśli nie jesteś zainteresowany wyjściem ...
Andreas Magnusson
2
@AndreasMagnusson: Konwencja wskaźnika jako parametru wyjściowego może dawać fałszywe poczucie bezpieczeństwa w przypadku wskaźników przekazywanych z innego źródła. Ponieważ w tym przypadku wywołanie jest zabawne (x) i * x jest modyfikowane, a źródło nie ma & x, aby cię ostrzec.
Zan Lynx
co się stanie, jeśli wywołanie funkcji przeżyje zakres wywołującego? Istnieje możliwość, że został wywołany destruktor udostępnionego ptr, a teraz funkcja będzie próbowała uzyskać dostęp do usuniętej pamięci, w wyniku czego UB
Sridhar Thiagarajan
4

Tak, cała idea shared_ptr <> polega na tym, że wiele instancji może przechowywać ten sam surowy wskaźnik, a podstawowa pamięć zostanie zwolniona tylko wtedy, gdy ostatnia instancja shared_ptr <> zostanie zniszczona.

Unikałbym wskaźnika do shared_ptr <>, ponieważ jest to sprzeczne z celem, ponieważ teraz znowu masz do czynienia z raw_pointers.

R Samuel Klatchko
źródło
2

Przekazywanie wartości w pierwszym przykładzie jest bezpieczne, ale jest lepszy idiom. Jeśli to możliwe, przekazuj przez odniesienie const - powiedziałbym, że tak, nawet w przypadku inteligentnych wskaźników. Twój drugi przykład nie jest całkiem zepsuty, ale jest bardzo !???. Głupie, nie osiągając niczego i pokonując część sensu inteligentnych wskazówek, i zostawiając cię w powolnym świecie bólu, gdy próbujesz wyłuskać i zmodyfikować rzeczy.

djechlin
źródło
3
Nikt nie jest zwolennikiem przekazywania przez wskaźnik, jeśli masz na myśli surowy wskaźnik. Przekazywanie przez odniesienie, jeśli nie ma sporu o własność, jest z pewnością idiomatyczne. Chodzi w shared_ptr<>szczególności o to, że musisz wykonać kopię, aby faktycznie być współwłasnością; jeśli masz odniesienie lub wskaźnik do a, shared_ptr<>to nie udostępniasz niczego i podlega tym samym problemom związanym z czasem życia, co normalne odniesienie lub wskaźnik.
ildjarn
2
@ildjarn: W tym przypadku przekazanie stałej referencji jest w porządku. Oryginał shared_ptrobiektu wywołującego ma gwarancję, że będzie trwał dłużej niż wywołanie funkcji, więc funkcja jest bezpieczna w użyciu shared_ptr<> const &. Jeśli zajdzie potrzeba przechowywania wskaźnika na później, będzie musiał skopiować (w tym momencie należy wykonać kopię, zamiast przechowywać odniesienie), ale nie ma potrzeby ponoszenia kosztów kopiowania, chyba że musisz to zrobić to. W tym przypadku chciałbym przekazać stałe referencje ....
David Rodríguez - dribeas
3
@David: „ Przekazanie stałej referencji jest w tym przypadku w porządku. ” Nie w żadnym rozsądnym API - dlaczego API miałby narzucać inteligentny typ wskaźnika, z którego nawet nie korzysta, zamiast po prostu przyjmować normalne odwołanie do stałej? To prawie tak samo złe, jak brak stałej poprawności. Jeśli chodzi o to, że technicznie nic to nie boli, to z pewnością się zgadzam, ale nie sądzę, aby było to właściwe.
ildjarn
2
... jeśli z drugiej strony funkcja nie musi przedłużać czasu życia obiektu, możesz po prostu usunąć shared_ptrz interfejsu i przekazać zwykłą ( const) referencję do wskazanego obiektu.
David Rodríguez - dribeas
3
@David: A co, jeśli Twój klient API nie używa shared_ptr<> na początku? Teraz twoje API jest naprawdę uciążliwe, ponieważ zmusza wywołującego do bezcelowej zmiany semantyki czasu życia obiektu tylko po to, aby go użyć. Nie widzę w tym nic wartego poparcia poza stwierdzeniem, że „technicznie nic to nie szkodzi”. Nie widzę nic kontrowersyjnego w kwestii „jeśli nie potrzebujesz współwłasności, nie używaj shared_ptr<>”.
ildjarn
0

w funkcji DoSomething zmieniasz element członkowski danych instancji klasy, myClass więc modyfikujesz obiekt zarządzany (nieprzetworzony wskaźnik), a nie obiekt (shared_ptr). Oznacza to, że w punkcie powrotu tej funkcji wszystkie współdzielone wskaźniki do zarządzanego wskaźnika surowego zobaczą swój element członkowski danych: myClass::someFieldzmieniony na inną wartość.

w tym przypadku przekazujesz obiekt funkcji z gwarancją, że jej nie modyfikujesz (mówimy o shared_ptr, a nie o posiadanym obiekcie).

Idiomem, który to wyraża, jest: a const ref, tak jak to

void DoSomething(const std::shared_ptr<myClass>& arg)

Podobnie zapewniasz użytkownika swojej funkcji, że nie dodajesz kolejnego właściciela do listy właścicieli surowego wskaźnika. Jednak zachowałeś możliwość modyfikacji podstawowego obiektu wskazywanego przez surowy wskaźnik.

CAVEAT: Co oznacza, że ​​jeśli w jakiś sposób ktoś shared_ptr::resetwywoła przed wywołaniem Twojej funkcji, aw tym momencie jest to ostatni shared_ptr będący właścicielem raw_ptr, to twój obiekt zostanie zniszczony, a twoja funkcja będzie manipulować wiszącym wskaźnikiem na zniszczonym obiekcie. BARDZO NIEBEZPIECZNE!!!

Organicoman
źródło