Czy powinniśmy przekazać shared_ptr przez referencję czy wartość?

270

Gdy funkcja przyjmuje wartość shared_ptr (z boost lub C ++ 11 STL), przekazujesz ją:

  • według stałej referencji: void foo(const shared_ptr<T>& p)

  • lub według wartości void foo(shared_ptr<T> p):?

Wolałbym pierwszą metodę, ponieważ podejrzewam, że byłaby szybsza. Ale czy to naprawdę jest tego warte, czy są jakieś dodatkowe problemy?

Czy możesz podać powody swojego wyboru lub, jeśli tak, dlaczego uważasz, że to nie ma znaczenia.

Danvil
źródło
14
Problem polega na tym, że nie są one równoważne. Wersja referencyjna krzyczy „Zamienię niektóre na aliasy shared_ptri mogę to zmienić, jeśli chcę.”, Podczas gdy wersja wartościowa mówi „Skopiuję twój shared_ptr, więc dopóki mogę to zmienić, nigdy się nie dowiesz. ) Parametr const-reference jest prawdziwym rozwiązaniem, które mówi: „Zamierzam trochę pseudonim shared_ptri obiecuję, że go nie zmienię.” (Który jest niezwykle podobny do semantyki według wartości!)
GManNickG
2
Hej, byłbym zainteresowany twoją opinią o powrocie członka shared_ptrklasy. Czy robisz to przez const-refs?
Johannes Schaub - litb
Trzecią możliwością jest użycie std :: move () z C ++ 0x, zamienia to zarówno shared_ptr
Tomaka17
@Johannes: Chciałbym zwrócić go przez const-referencja, aby uniknąć kopiowania / ponownego liczenia. Z drugiej strony zwracam wszystkich członków przez stałe odwołanie, chyba że są prymitywne.
GManNickG
możliwy duplikat C ++ - pytanie przechodzące wskaźnik
kennytm

Odpowiedzi:

229

To pytanie zostało omówione i udzielone przez Scott, Andrei i Herb podczas sesji Ask Us Anything w C ++ i Beyond 2011 . Obejrzyj od 4:34 na temat shared_ptrwydajności i poprawności .

Krótko mówiąc, nie ma powodu, by przekazywać wartość, chyba że celem jest współwłasność własności obiektu (np. Między różnymi strukturami danych lub między różnymi wątkami).

O ile nie możesz go zoptymalizować w ruchu, jak wyjaśnił Scott Meyers w powyższym filmie dyskusyjnym, ale jest to związane z faktyczną wersją C ++, której możesz użyć.

Główna aktualizacja tej dyskusji nastąpiła podczas interaktywnego panelu konferencji GoingNative 2012 : Zapytaj nas o wszystko! co warto obejrzeć, zwłaszcza od 22:50 .

mloskot
źródło
5
ale jak pokazano tutaj, tańsze jest przekazywanie według wartości: stackoverflow.com/a/12002668/128384 nie powinien być również brany pod uwagę (przynajmniej w przypadku argumentów konstruktora itp., w których element shared_ptr ma zostać członkiem klasa)?
stijn
2
@stijn Tak i nie. Wskazane pytania i odpowiedzi są niekompletne, chyba że wyjaśnią wersję standardu C ++, do której się odnoszą. Bardzo łatwo jest rozpowszechniać ogólne zasady nigdy / zawsze wprowadzające w błąd. Chyba że czytelnicy poświęcają czas na zapoznanie się z artykułem i referencjami Davida Abrahamsa lub biorą pod uwagę datę opublikowania w stosunku do obecnego standardu C ++. Tak więc obie odpowiedzi, moja i ta, którą wskazałeś, są poprawne, biorąc pod uwagę czas wysłania.
mloskot
1
chyba że jest wielowątkowość ” nie, MT nie jest w żaden sposób wyjątkowy.
ciekawy,
3
Jestem spóźniony na imprezę, ale moim powodem, dla którego chcę przekazać wartość parametru shared_ptr, jest to, że kod jest krótszy i ładniejszy. Poważnie. Value*jest krótki i czytelny, ale zły, więc teraz mój kod jest pełny const shared_ptr<Value>&i jest znacznie mniej czytelny i po prostu ... mniej uporządkowany. To, co kiedyś było, void Function(Value* v1, Value* v2, Value* v3)jest teraz void Function(const shared_ptr<Value>& v1, const shared_ptr<Value>& v2, const shared_ptr<Value>& v3), a ludzie się z tym zgadzają?
Alex
7
@Alex Powszechną praktyką jest tworzenie aliasów (typedefs) zaraz po zajęciach. Na przykład: class Value {...}; using ValuePtr = std::shared_ptr<Value>;Twoja funkcja staje się prostsza: void Function(const ValuePtr& v1, const ValuePtr& v2, const ValuePtr& v3)i uzyskujesz maksymalną wydajność. Dlatego używasz C ++, prawda? :)
4LegsDrivenCat
91

Oto ujęcie Herb Suttera

Wskazówka: Nie przekazuj inteligentnego wskaźnika jako parametru funkcji, chyba że chcesz używać lub manipulować samym inteligentnym wskaźnikiem, na przykład w celu współdzielenia lub przeniesienia własności.

Wytyczna: Wyraź, że funkcja będzie przechowywać i współdzielić własność obiektu sterty za pomocą parametru wartości_podzielonej by-value.

Wskazówka: Użyj nie-const parametru shared_ptr i tylko do zmodyfikowania parametru shared_ptr. Użyj stałej shared_ptr & jako parametru tylko wtedy, gdy nie masz pewności, czy zrobisz kopię i udostępnisz własność; w przeciwnym razie użyj widget * (lub jeśli nie ma wartości null, widget &).

acel
źródło
3
Dzięki za link do Suttera. To jest doskonały artykuł. Nie zgadzam się z nim w sprawie widgetu *, wolę opcjonalny <widget &>, jeśli C ++ 14 jest dostępny. widget * jest zbyt dwuznaczny w stosunku do starego kodu.
tytuł z
3
+1 za włączenie widżetu * i widżetu oraz jako możliwości. Po prostu opracowanie, przekazanie widgetu * lub widgetu & jest prawdopodobnie najlepszą opcją, gdy funkcja nie sprawdza / nie modyfikuje samego obiektu wskaźnika. Interfejs jest bardziej ogólny, ponieważ nie wymaga określonego typu wskaźnika, a problem z wydajnością licznika referencji shared_ptr jest unikany.
tgnottingham
4
Myślę, że powinna to być dzisiaj akceptowana odpowiedź z powodu drugiej wytycznej. Wyraźnie unieważnia aktualną akceptowaną odpowiedź, która mówi: nie ma powodu, by przekazywać wartość.
mbrt
63

Osobiście użyłbym constreferencji. Nie ma potrzeby zwiększania liczby referencyjnej tylko po to, aby ją ponownie zmniejszyć ze względu na wywołanie funkcji.

Evan Teran
źródło
1
Nie głosowałem za odrzuceniem twojej odpowiedzi, ale zanim będzie to kwestia preferencji, istnieją dwie zalety i wady każdej z dwóch możliwości do rozważenia. I dobrze byłoby poznać i omówić te za i przeciw. Następnie każdy może sam podjąć decyzję.
Danvil
@ Danvil: biorąc pod uwagę sposób shared_ptrdziałania, jedynym możliwym minusem nieprzekazania odniesienia jest niewielka utrata wydajności. Są tu dwie przyczyny. a) funkcja aliasingu wskaźnika oznacza, że ​​dane są warte wskaźników, a licznik (może 2 dla słabych referencji) jest kopiowany, więc kopiowanie danych jest nieco droższe. b) zliczanie referencji atomowych jest nieco wolniejsze niż zwykły stary kod inkrementacji / dekrementacji, ale jest potrzebne, aby zapewnić bezpieczeństwo wątków. Poza tym obie metody są takie same dla większości celów i celów.
Evan Teran
37

Przekaż przez constodniesienie, jest szybszy. Jeśli chcesz go przechowywać, powiedzmy w jakimś pojemniku, ref. liczba zostanie automatycznie zwiększona przez operację kopiowania.

Nikołaj Fetissow
źródło
4
Downvote ma swoją opinię bez żadnych liczb, aby ją poprzeć.
kwesolowski
22

Uruchomiłem poniższy kod, raz z fooprzyjmowaniem wartości „ shared_ptrby by” const&i „raz po raz” z fooprzyjmowaniem shared_ptrwartości według.

void foo(const std::shared_ptr<int>& p)
{
    static int x = 0;
    *p = ++x;
}

int main()
{
    auto p = std::make_shared<int>();
    auto start = clock();
    for (int i = 0; i < 10000000; ++i)
    {
        foo(p);
    }    
    std::cout << "Took " << clock() - start << " ms" << std::endl;
}

Używając wersji VS2015, x86, na moim czterordzeniowym procesorze Intel Core 2 (2,4 GHz)

const shared_ptr&     - 10ms  
shared_ptr            - 281ms 

Wersja kopiowania według wartości była wolniejsza o rząd wielkości.
Jeśli wywołujesz funkcję synchronicznie z bieżącego wątku, wybierz const&wersję.

tbb
źródło
1
Czy możesz powiedzieć, jakich ustawień kompilatora, platformy i optymalizacji użyłeś?
Carlton,
Użyłem kompilacji debugowania vs2015, zaktualizowałem odpowiedź, aby użyć kompilacji wersji teraz.
tcb
1
Jestem ciekawy, czy po włączeniu optymalizacji uzyskasz te same wyniki w obu przypadkach
Elliot Woods
2
Optymalizacja niewiele pomaga. problemem jest rywalizacja o blokadę na podstawie liczby referencji na kopii.
Alex
1
Nie o to chodzi. Taka foo()funkcja nie powinna nawet w ogóle przyjmować wspólnego wskaźnika, ponieważ nie korzysta z tego obiektu: powinna akceptować int&i robić p = ++x;, wywołując foo(*p);z main(). Funkcja akceptuje inteligentny obiekt wskaźnika, gdy musi coś z nim zrobić, i przez większość czasu musisz go przenieść ( std::move()) w inne miejsce, więc parametr według wartości nie kosztuje.
eepp
14

Od C ++ 11 powinieneś brać to pod względem wartości ponad const i częściej niż myślisz.

Jeśli bierzesz std :: shared_ptr (zamiast bazowego typu T), robisz to, ponieważ chcesz coś z tym zrobić.

Jeśli chcesz go gdzieś skopiować , sensowniej jest zabrać go za pomocą kopiowania i przenieść std :: wewnętrznie, zamiast zabierać to przez const &, a następnie kopiowanie. Jest tak, ponieważ pozwalasz wywołującemu opcję z kolei std :: move shared_ptr podczas wywoływania twojej funkcji, oszczędzając w ten sposób sobie zestaw operacji zwiększania i zmniejszania. Albo nie. Oznacza to, że wywołujący funkcję może zdecydować, czy potrzebuje std :: shared_ptr po wywołaniu funkcji, w zależności od tego, czy się poruszy, czy nie. Nie jest to możliwe, jeśli przejdziesz przez const &, a zatem najlepiej jest przyjąć wartość.

Oczywiście, jeśli osoba wywołująca potrzebuje dłużej swojej shared_ptr (więc nie może std :: move it) i nie chcesz tworzyć zwykłej kopii w funkcji (powiedz, że chcesz słaby wskaźnik, lub tylko czasami chcesz aby go skopiować, w zależności od niektórych warunków), wtedy const i może być nadal preferowane.

Na przykład powinieneś to zrobić

void enqueue(std::shared<T> t) m_internal_queue.enqueue(std::move(t));

nad

void enqueue(std::shared<T> const& t) m_internal_queue.enqueue(t);

Ponieważ w takim przypadku zawsze tworzysz kopię wewnętrznie

Ciastko
źródło
1

Nie znając kosztu czasu operacji kopiowania shared_copy, w której występuje przyrost i spadek atomowy, cierpiałem z powodu znacznie większego problemu z użyciem procesora. Nigdy nie spodziewałem się, że przyrost atomowy i dekrementacja mogą kosztować tyle.

Po moim teście, przyrost i spadek atomowy int32 zajmuje 2 lub 40 razy niż wzrost i spadek nieatomowy. Mam go na Core i7 3GHz z Windows 8.1. Pierwszy wynik pojawia się, gdy nie dochodzi do rywalizacji, drugi, gdy występuje duża możliwość rywalizacji. Pamiętam, że operacje atomowe są w końcu blokadą sprzętową. Zamek jest zamkiem. Zła wydajność, gdy występuje rywalizacja.

Doświadczając tego, zawsze używam byref (const shared_ptr &) niż byval (shared_ptr).

Hyunjik Bae
źródło
0

Był ostatni post na blogu: https://medium.com/@vgasparyan1995/pass-by-value-vs-pass-by-reference-to-const-c-f8944171e3ce

Odpowiedź na to brzmi: nigdy (prawie) nigdy nie omijaj const shared_ptr<T>&.
Zamiast tego po prostu przekaż klasę bazową.

Zasadniczo jedynymi rozsądnymi typami parametrów są:

  • shared_ptr<T> - Zmień i przejmij własność
  • shared_ptr<const T> - Nie modyfikuj, przejmuj na własność
  • T& - Zmień, bez prawa własności
  • const T& - Nie modyfikuj, brak prawa własności
  • T - Nie modyfikuj, nie posiadaj, tanie do skopiowania

Jak zauważył @accel w https://stackoverflow.com/a/26197326/1930508 rada Herb Sutter jest następująca:

Użyj stałej shared_ptr & jako parametru tylko wtedy, gdy nie masz pewności, czy zrobisz kopię i udostępnisz własność

Ale w ilu przypadkach nie jesteś pewien? To rzadka sytuacja

Flamefire
źródło
0

Znany jest problem polegający na tym, że przekazywanie wartości parametru shared_ptr wiąże się z pewnymi kosztami i należy go w miarę możliwości unikać.

Koszt przejścia przez shared_ptr

Przeważnie wystarczyłoby dzielenie share_ptr przez referencję, a jeszcze lepiej przez const referen.

Podstawowa wytyczna cpp ma określoną zasadę przekazywania komendy shared_ptr

R.34: Weź parametr shared_ptr, aby wyrazić, że funkcja jest właścicielem części

void share(shared_ptr<widget>);            // share -- "will" retain refcount

Przykładem, gdy przekazanie parametru shared_ptr przez wartość jest naprawdę konieczne, jest to, gdy obiekt wywołujący przekazuje obiekt współdzielony do asynchronicznego odbiorcy - tzn. Dzwoniący wykracza poza zakres, zanim odbiorca zakończy swoje zadanie. Odbiorca musi „przedłużyć” żywotność współdzielonego obiektu, biorąc wartość share_ptr według wartości. W takim przypadku przekazanie odwołania do shared_ptr nie zadziała.

To samo dotyczy przekazywania współdzielonego obiektu do wątku roboczego.

artm
źródło
-4

shared_ptr nie jest wystarczająco duży, ani jego konstruktor \ destruktor nie wykonuje wystarczającej pracy, aby z kopii było wystarczające obciążenie, aby dbać o przekazywanie przez odniesienie w porównaniu do wydajności kopiowania przez kopię.

kamieniste
źródło
15
Zmierzyłeś to?
ciekawy,
2
@stonemetal: A co z instrukcjami atomowymi podczas tworzenia nowego shared_ptr?
Quarra
Jest to typ inny niż POD, więc w większości ABI nawet przekazanie go „według wartości” faktycznie mija wskaźnik. Problemem nie jest faktyczne kopiowanie bajtów. Jak widać na wyjściu asm, przekazanie shared_ptr<int>wartości przez zajmuje 100 instrukcji x86 (w tym drogich lockinstrukcji ed, aby atomowo zwiększyć / zmniejszyć liczbę referencji). Przekazywanie przez stałą referencję jest takie samo, jak przekazywanie wskaźnika do czegokolwiek (w tym przykładzie w eksploratorze kompilatora Godbolt optymalizacja wywołania ogona zamienia to w zwykły plik jmp zamiast wywołania: godbolt.org/g/TazMBU ).
Peter Cordes,
TL: DR: To jest C ++, w którym konstruktory kopiowania mogą wykonać znacznie więcej pracy niż tylko kopiowanie bajtów. Ta odpowiedź to śmieci.
Peter Cordes,
2
stackoverflow.com/questions/3628081/shared-ptr-horrible-speed Jako przykład Udostępnione wskaźniki przekazane przez wartość vs przekazanie przez referencję, widzi różnicę czasu działania wynoszącą około 33%. Jeśli pracujesz nad kodem krytycznym dla wydajności, to nagie wskaźniki zwiększają wydajność. Więc pamiętaj o obejrzeniu const ref, jeśli pamiętasz, ale nie jest to wielka sprawa, jeśli nie. O wiele ważniejsze jest, aby nie używać shared_ptr, jeśli go nie potrzebujesz.
stonemetal