Słyszałem niedawno rozmowę przez Herb Sutter, który zasugerował, że powody, aby przejść std::vector
i std::string
przez const &
są w dużej mierze zniknęły. Zasugerował, że lepiej jest teraz napisać funkcję taką jak poniżej:
std::string do_something ( std::string inval )
{
std::string return_val;
// ... do stuff ...
return return_val;
}
Rozumiem, że return_val
będzie to wartość w punkcie, w którym funkcja zwraca wartość, dlatego można ją zwrócić za pomocą semantyki przenoszenia, która jest bardzo tania. Jest jednak inval
nadal znacznie większy niż rozmiar odwołania (który zwykle jest implementowany jako wskaźnik). Wynika to z faktu, że a std::string
ma różne składniki, w tym wskaźnik do sterty i element char[]
do optymalizacji krótkiego łańcucha. Wydaje mi się więc, że przekazywanie referencji jest nadal dobrym pomysłem.
Czy ktoś może wyjaśnić, dlaczego Herb mógł to powiedzieć?
Odpowiedzi:
Powodem, dla którego Herb powiedział, jest to z powodu takich przypadków.
Powiedzmy, że mam funkcję,
A
która wywołuje funkcjęB
, która wywołuje funkcjęC
. IA
przechodzi sznurkiem doB
i doC
.A
nie wie lub nie dba o toC
; wszystkoA
o czym jest wiadomoB
. To jestC
szczegół implementacjiB
.Powiedzmy, że A jest zdefiniowane następująco:
Jeśli B i C zabiorą ciąg
const&
, to wygląda mniej więcej tak:Wszystko dobrze i dobrze. Po prostu podajesz wskazówki, nie kopiujesz, nie ruszasz się, wszyscy są szczęśliwi.
C
bierze,const&
ponieważ nie przechowuje ciągu. Po prostu go używa.Teraz chcę wprowadzić jedną prostą zmianę:
C
trzeba gdzieś zapisać ciąg.Witaj, skopiuj konstruktor i potencjalny przydział pamięci (zignoruj Optymalizację krótkich ciągów (SSO) ). Semantyka ruchów w C ++ 11 ma umożliwić usunięcie niepotrzebnego konstruowania kopii, prawda? I
A
przemija chwilowo; nie ma powodu, dla któregoC
należy skopiować dane. Powinien po prostu uciekać z tego, co zostało mu dane.Tyle że nie może. Ponieważ to zajmuje
const&
.Jeśli zmienię,
C
aby wziąć jego parametr według wartości, spowoduje to po prostuB
skopiowanie do tego parametru; Nic nie zyskuję.Więc gdybym właśnie przekazał
str
wartość przez wszystkie funkcje, polegając nastd::move
przetasowaniu danych, nie mielibyśmy tego problemu. Jeśli ktoś chce się tego trzymać, może. Jeśli nie, no cóż.Czy to jest droższe? Tak; przejście do wartości jest droższe niż korzystanie z referencji. Czy to jest tańsze niż kopia? Nie dla małych ciągów z jednokrotnym logowaniem. Czy warto to zrobić?
To zależy od twojego przypadku użycia. Jak bardzo nienawidzisz alokacji pamięci?
źródło
moved
od B do C w przypadku według wartości? Jeśli B jest,B(std::string b)
a C jestC(std::string c)
, musimy albo wezwaćC(std::move(b))
B, albob
pozostać niezmienione (a więc „niewzruszone”) aż do wyjściaB
. (Być może optymalizacji kompilator ruszy ciąg pod as-jeśli reguła, jeślib
nie jest stosowany po zakończeniu rozmowy, ale nie sądzę, istnieje silna gwarancja). To samo odnosi się do kopiistr
dom_str
. Nawet jeśli parametr funkcji został zainicjowany wartością rvalue, jest to wartość wewnątrz funkcji istd::move
wymagane jest przejście z tej wartości.Nie . Wiele osób stosuje tę radę (w tym Dave'a Abrahama) poza dziedziną, której dotyczy, i upraszcza ją, aby stosować do wszystkich
std::string
parametrów - Zawsze przekazywaniestd::string
wartości nie jest „najlepszą praktyką” dla dowolnych dowolnych parametrów i aplikacji, ponieważ ich optymalizacje rozmowy / artykuły dotyczą tylko ograniczonego zestawu przypadków .Jeśli zwracasz wartość, mutujesz parametr lub bierzesz wartość, to przekazywanie wartości może zaoszczędzić kosztownego kopiowania i zaoferować wygodę syntaktyczną.
Jak zawsze, przejście przez odniesienie do stałej oszczędza dużo kopiowania, gdy nie potrzebujesz kopii .
Teraz konkretny przykład:
Jeśli rozmiar stosu stanowi problem (i przy założeniu, że nie jest on wbudowany / zoptymalizowany),
return_val
+inval
>return_val
- IOW, szczytowe użycie stosu można zmniejszyć , przekazując tutaj wartość (uwaga: nadmierne uproszczenie ABI). Tymczasem pominięcie stałej referencji może wyłączyć optymalizacje. Głównym powodem tutaj nie jest unikanie wzrostu stosu, ale zapewnienie, że optymalizacja może być wykonana tam, gdzie ma to zastosowanie .Dni mijania stałej referencji jeszcze się nie skończyły - zasady były bardziej skomplikowane niż kiedyś. Jeśli wydajność jest ważna, warto rozważyć sposób przekazywania tych typów, w oparciu o szczegóły używane w implementacjach.
źródło
Zależy to w dużej mierze od implementacji kompilatora.
Jednak zależy to również od tego, czego używasz.
Rozważmy kolejne funkcje:
Funkcje te są zaimplementowane w osobnej jednostce kompilacyjnej, aby uniknąć wstawiania. Następnie:
1. Jeśli przekażesz dosłownie te dwie funkcje, nie zobaczysz dużej różnicy w wydajności. W obu przypadkach należy utworzyć obiekt łańcuchowy
2. Jeśli przekażesz inny obiekt std :: string,
foo2
osiągnie lepsze wynikifoo1
, ponieważfoo1
wykona głęboką kopię.Na moim komputerze, używając g ++ 4.6.1, otrzymałem następujące wyniki:
źródło
Krótka odpowiedź: NIE! Długa odpowiedź:
const ref&
.(
const ref&
oczywiście musi pozostać w zasięgu podczas wykonywania funkcji, która go używa)value
, nie kopiujconst ref&
wnętrza funkcji.Na stronie cpp-next.com pojawił się post zatytułowany „Chcesz prędkości przekazać wartość!” . TL; DR:
TŁUMACZENIE ^
Nie kopiuj argumentów funkcji --- oznacza: jeśli planujesz zmodyfikować wartość argumentu, kopiując ją do zmiennej wewnętrznej, po prostu użyj argumentu wartości .
Więc nie rób tego :
zrób to :
Kiedy trzeba zmodyfikować wartość argumentu w treści funkcji.
Musisz tylko wiedzieć, jak planujesz użyć argumentu w treści funkcji. Tylko do odczytu lub NIE ... i jeśli mieści się w zakresie.
źródło
const ref&
do zmiennej wewnętrznej, aby ją zmodyfikować. Jeśli musisz go zmodyfikować ... ustaw parametr na wartość. Jest to raczej jasne dla mojej osoby nieanglojęzycznej.const ref&
. # 2 Jeśli muszę to napisać lub wiem, że wychodzi poza zakres ... Używam wartości. # 3 Jeśli muszę zmodyfikować pierwotną wartość, mijamref&
. # 4 Używam,pointers *
jeśli argument jest opcjonalny, więc mogę tonullptr
zrobić.O ile nie potrzebujesz kopii, nadal warto ją zabrać
const &
. Na przykład:Jeśli zmienisz to, aby ciąg znaków był przyjmowany według wartości, skończysz na przenoszeniu lub kopiowaniu parametru i nie ma takiej potrzeby. Kopiowanie / przenoszenie jest nie tylko prawdopodobnie droższe, ale także wprowadza nową potencjalną awarię; kopiowanie / przenoszenie może spowodować wyjątek (np. alokacja podczas kopiowania może się nie powieść), podczas gdy odwołanie do istniejącej wartości nie może.
Jeśli nie potrzebujesz kopię następnie przechodząc i powrót przez wartość jest zazwyczaj (zawsze?) Najlepszą opcję. W zasadzie nie martwiłbym się tym w C ++ 03, chyba że odkryjesz, że dodatkowe kopie powodują problem z wydajnością. Kopiowanie kopii wydaje się dość niezawodne w nowoczesnych kompilatorach. Myślę, że sceptycyzm ludzi i naleganie, aby sprawdzić tabelę obsługi kompilatora dla RVO, jest obecnie w większości przestarzałe.
Krótko mówiąc, C ++ 11 tak naprawdę niczego nie zmienia pod tym względem, z wyjątkiem ludzi, którzy nie ufali kopiowaniu elision.
źródło
noexcept
, ale oczywiście konstruktory kopiujące nie są.Prawie.
W C ++ 17 mamy
basic_string_view<?>
, co sprowadza nas do jednego wąskiego przypadku użyciastd::string const&
parametrów.Istnienie semantyki ruchu wyeliminowało jeden przypadek użycia
std::string const&
- jeśli planujesz przechowywać parametr, przyjęciestd::string
wartości według jest bardziej optymalne, ponieważ możeszmove
wyjść z parametru.Jeśli ktoś nazwał twoją funkcję surowym C,
"string"
oznacza to, żestd::string
przydzielony jest tylko jeden bufor, w przeciwieństwie do dwóch w tymstd::string const&
przypadku.Jeśli jednak nie zamierzasz robić kopii, przyjmowanie
std::string const&
jest nadal przydatne w C ++ 14.Dzięki
std::string_view
, o ile nie przekazujesz wspomnianego ciągu do interfejsu API, który oczekuje'\0'
buforowanych znaków w stylu C , możesz wydajniej uzyskaćstd::string
funkcjonalność bez ryzyka alokacji. Surowy ciąg C może nawet zostać przekształcony wstd::string_view
bez przydziału lub kopiowania znaków.W tym momencie jest to przydatne,
std::string const&
gdy nie kopiujesz danych hurtowo i zamierzasz przekazać je do interfejsu API w stylu C, który oczekuje bufora zakończonego zerem i potrzebujesz funkcji łańcucha wyższego poziomu, którestd::string
zapewniają. W praktyce jest to rzadki zestaw wymagań.źródło
std::string
aby dominować, potrzebujesz nie tylko niektórych z tych wymagań, ale wszystkich z nich. Przyznaję, że jeden lub dwa z nich są powszechne. Może zwykle potrzebujesz wszystkich 3 (np.std::string_view
- jak zauważyłeś, „surowy ciąg C można nawet przekształcić w widok std :: string_view bez przypisania lub kopiowania znaków”, o czym warto pamiętać tych, którzy używają C ++ w kontekście takie użycie API rzeczywiście.std::string
nie jest zwykłymi starymi danymi (POD) , a jego surowy rozmiar nie jest najważniejszą rzeczą w historii. Na przykład jeśli przekażesz ciąg znaków, który jest dłuższy niż SSO i przydzielony na stercie, oczekiwałbym, że konstruktor kopiowania nie skopiuje pamięci SSO.Jest to zalecane dlatego, że
inval
jest zbudowane z wyrażenia argumentu, a zatem zawsze jest odpowiednio przenoszone lub kopiowane - nie występuje utrata wydajności, przy założeniu, że potrzebujesz własności argumentu. Jeśli tego nie zrobisz,const
referencje mogą być lepszym sposobem.źródło
std::string<>
z stosu przydzielane są 32 bajty, z których 16 należy zainicjować. Porównaj to z zaledwie 8 bajtami przydzielonymi i zainicjowanymi dla odniesienia: to dwukrotnie więcej pracy procesora i zajmuje cztery razy więcej miejsca w pamięci podręcznej, które nie będzie dostępne dla innych danych.Skopiowałem / wkleiłem odpowiedź z tego pytania tutaj i zmieniłem nazwy i pisownię, aby pasowały do tego pytania.
Oto kod do pomiaru tego, o co się pyta:
Dla mnie ten wynik:
Poniższa tabela podsumowuje moje wyniki (używając clang -std = c ++ 11). Pierwsza liczba to liczba konstrukcji kopii, a druga liczba to liczba konstrukcji ruchu:
Rozwiązanie typu pass-by-value wymaga tylko jednego przeciążenia, ale kosztuje dodatkową konstrukcję ruchu podczas przekazywania wartości lvalu i xvalues. Może to być lub nie do przyjęcia w danej sytuacji. Oba rozwiązania mają zalety i wady.
źródło
Herb Sutter jest nadal zapisywany, wraz z Bjarne Stroustroup, polecając
const std::string&
jako typ parametru; patrz https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Rf-in .Istnieje pułapka niewymieniona w żadnej z innych odpowiedzi tutaj: jeśli przekażesz literałowi ciąg znaków do
const std::string&
parametru, przekaże on odwołanie do tymczasowego ciągu, utworzonego w locie, aby przechować znaki literału. Jeśli następnie zapiszesz to odwołanie, będzie ono nieważne po zwolnieniu tymczasowego łańcucha. Aby być bezpiecznym, musisz zapisać kopię , a nie referencję. Problem wynika z faktu, że literały łańcuchowe sąconst char[N]
typami, które wymagają promocjistd::string
.Poniższy kod ilustruje pułapkę i obejście, a także niewielką opcję wydajności - przeładowanie
const char*
metodą, jak opisano w Czy istnieje sposób na przekazanie literału ciągu jako odwołania w C ++ .(Uwaga: Sutter i Stroustroup radzą, że jeśli zachowasz kopię łańcucha, zapewnij również przeciążoną funkcję z parametrem && i std :: move () it.)
WYNIK:
źródło
T&&
nie zawsze jest to uniwersalne odniesienie; w rzeczywistościstd::string&&
zawsze będzie to odwołanie do wartości, a nie odwołanie uniwersalne, ponieważ nie jest przeprowadzana dedukcja typu . Tak więc rada Stroustroup & Sutter nie jest sprzeczna z opinią Meyersa.T&&
jest dedukcyjnym uniwersalnym odniesieniem, czy niededukowanym odniesieniem do wartości; prawdopodobnie byłoby to jaśniejsze, gdyby wprowadzili inny symbol uniwersalnych odniesień (takich jak&&&
kombinacja&
i&&
), ale prawdopodobnie wyglądałoby to po prostu głupio.IMO wykorzystujące odwołanie do C ++
std::string
to szybka i krótka lokalna optymalizacja, a użycie przekazywania wartości może być (lub nie) lepszą globalną optymalizacją.Odpowiedź brzmi: zależy od okoliczności:
const std::string &
.std::string
zachowaniu konstruktora kopii.źródło
Zobacz „Herb Sutter” Powrót do podstaw! Podstawy nowoczesnego stylu C ++ ” . Między innymi przegląda wskazówki dotyczące przekazywania parametrów podane w przeszłości oraz nowe pomysły, które pojawiają się w C ++ 11, a szczególnie patrzy na idea przekazywania ciągów według wartości.
Testy porównawcze pokazują, że przekazywanie
std::string
wartości s, w każdym przypadku, gdy funkcja i tak ją skopiuje, może być znacznie wolniejsze!Dzieje się tak, ponieważ zmuszasz go do wykonania pełnej kopii (a następnie przejścia na miejsce), podczas gdy
const&
wersja zaktualizuje stary ciąg znaków, co może ponownie wykorzystać już przydzielony bufor.Zobacz jego slajd 27: W przypadku funkcji „ustawiania” opcja 1 jest taka sama, jak zawsze. Opcja 2 dodaje przeciążenie dla wartości odniesienia, ale daje to kombinatoryczną eksplozję, jeśli istnieje wiele parametrów.
Tylko w przypadku parametrów „sink”, w których należy utworzyć ciąg (bez zmiany jego istniejącej wartości), sztuczka pass-by-value jest poprawna. Oznacza to, że konstruktory, w których parametr bezpośrednio inicjuje element pasującego typu.
Jeśli chcesz zobaczyć, jak głęboko możesz się martwić o to, obejrzyj prezentację Nicolai Josuttisa i powodzenia z nią ( „Idealne - zrobione!” N razy po znalezieniu błędu w poprzedniej wersji. Czy kiedykolwiek tam byłeś?)
Jest to również streszczone jako „ F.15 w Standardowych Wytycznych.
źródło
Jak zauważył @ JDługosz w komentarzach, Herb udziela innych rad w innym (później?) Przemówieniu, patrz mniej więcej stąd: https://youtu.be/xnqTKD8uD64?t=54m50s .
Jego rada sprowadza się tylko do używania parametrów wartości dla funkcji,
f
która pobiera tak zwane argumenty sink, zakładając, że przeniesiesz konstrukcję z tych argumentów sink.To ogólne podejście dodaje tylko narzut konstruktora ruchu zarówno dla argumentów lvalue, jak i rvalue w porównaniu z optymalną implementacją odpowiednio
f
dostosowanych argumentów lvalue i rvalue. Aby zobaczyć, dlaczego tak jest, załóżmy, żef
bierze się parametr wartości, gdzieT
jest jakiś możliwy do skopiowania i przeniesienia typ konstrukcyjny:Wywołanie
f
z argumentem lvalue spowoduje wywołanie konstruktora kopii w celu skonstruowaniax
i konstruktora ruchu w celu skonstruowaniay
. Z drugiej strony, wywołanief
z argumentem rvalue spowoduje wywołanie konstruktora ruchu w celu skonstruowaniax
i innego konstruktora ruchu w celu skonstruowaniay
.Zasadniczo optymalna implementacja
f
argumentów lvalue jest następująca:W takim przypadku do konstruowania wywoływany jest tylko jeden konstruktor kopii
y
. Optymalna implementacjaf
argumentów dla wartości jest znowu ogólnie następująca:W tym przypadku do konstruowania wywoływany jest tylko jeden konstruktor ruchu
y
.Rozsądnym kompromisem jest więc przyjęcie parametru wartości i wywołanie dodatkowego konstruktora ruchu dla argumentów lvalue lub rvalue w odniesieniu do optymalnej implementacji, co jest również wskazówką zawartą w wykładzie Herb.
Jak zauważył @ JDługosz w komentarzach, przekazywanie przez wartość ma sens tylko dla funkcji, które zbudują jakiś obiekt z argumentu sink. Jeśli masz funkcję,
f
która kopiuje swój argument, podejście polegające na przekazywaniu przez wartość będzie miało większy narzut niż ogólne podejście polegające na przekazywaniu przez stałą. Metoda przekazywania wartości dla funkcji,f
która zachowuje kopię swojego parametru, będzie miała postać:W takim przypadku istnieje konstrukcja kopiowania i przypisanie ruchu dla argumentu wartości, a także przeniesienie budowy i przypisanie ruchu dla argumentu wartości. Najbardziej optymalnym przypadkiem argumentu lvalue jest:
Sprowadza się to tylko do przypisania, które jest potencjalnie znacznie tańsze niż konstruktor kopiowania plus przypisanie do ruchu wymagane w podejściu typu pass-by-value. Powodem tego jest to, że przypisanie może ponownie wykorzystać istniejącą przydzieloną pamięć
y
, a tym samym zapobiegać (de) przydziałom, podczas gdy konstruktor kopiowania zwykle przydziela pamięć.Dla argumentu wartości najbardziej optymalna implementacja,
f
która zachowuje kopię, ma postać:Zatem w tym przypadku tylko przypisanie ruchu. Przekazanie wartości do tej wersji
f
wymaga stałej referencji kosztuje tylko przypisanie zamiast przypisania ruchu. Mówiąc relatywnie,f
preferowana jest wersja przyjmowania stałego odniesienia w tym przypadku jako ogólnej implementacji.Ogólnie rzecz biorąc, aby uzyskać najbardziej optymalną implementację, konieczne będzie przeładowanie lub wykonanie pewnego rodzaju doskonałego przekazywania, jak pokazano w wykładzie. Wadą jest kombinatoryczna eksplozja w liczbie wymaganych przeciążeń, w zależności od liczby parametrów, na
f
wypadek, gdyby zdecydowano się na przeciążenie kategorii wartości argumentu. Idealne przekazywanie ma tę wadę, żef
staje się funkcją szablonu, która zapobiega wirtualizacji i skutkuje znacznie bardziej złożonym kodem, jeśli chcesz uzyskać go w 100% poprawnie (zobacz szczegóły dotyczące krwawych szczegółów).źródło
Problem polega na tym, że „const” jest nie granulowanym kwalifikatorem. Co zwykle oznacza „const string ref” to „nie modyfikuj tego ciągu”, a nie „nie modyfikuj liczby referencji”. W C ++ po prostu nie można powiedzieć, którzy członkowie są „const”. Oni wszyscy są lub żaden z nich nie jest.
Aby włamać się do tego problemu językowego, STL może zezwolić „C ()” w twoim przykładzie na wykonanie kopii semantycznej i mimo to posłusznie zignorować „const” w odniesieniu do liczby referencji (zmienne). Tak długo, jak będzie to dobrze określone, będzie dobrze.
Ponieważ STL tego nie robi, mam wersję ciągu, który const_casts <> odsyła licznik referencji (nie ma sposobu, aby wstecznie zmienić coś w hierarchii klas) i - oto i oto - możesz swobodnie przekazywać cmstring jako stałe odwołania, i rób ich kopie w głębokich funkcjach, przez cały dzień, bez wycieków i problemów.
Ponieważ C ++ nie oferuje tutaj „ziarnistości stałej stałej klasy”, najlepszym rozwiązaniem, jakie widziałem, jest napisanie dobrej specyfikacji i zrobienie nowego błyszczącego nowego obiektu „const ruchomy ciąg” (cmstring).
źródło