Czy minęły już dni przekazywania const std :: string & jako parametru?

604

Słyszałem niedawno rozmowę przez Herb Sutter, który zasugerował, że powody, aby przejść std::vectori std::stringprzez 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_valbę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 invalnadal znacznie większy niż rozmiar odwołania (który zwykle jest implementowany jako wskaźnik). Wynika to z faktu, że a std::stringma 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ć?

Benj
źródło
89
Myślę, że najlepszą odpowiedzią na to pytanie jest prawdopodobnie przeczytanie artykułu Dave'a Abrahamsa na ten temat w C ++ Next . Dodałbym, że nie widzę w tym nic, co można by uznać za nie na temat lub niekonstruktywne. To jasne pytanie o programowanie, na które są prawdziwe odpowiedzi.
Jerry Coffin,
Fascynujące, więc jeśli i tak będziesz musiał zrobić kopię, przekazanie przez wartość jest prawdopodobnie szybsze niż przekazanie przez odniesienie.
Benj
3
@Sz. Jestem wrażliwy na pytania, które są fałszywie klasyfikowane jako duplikaty i zamknięte. Nie pamiętam szczegółów tej sprawy i nie sprawdziłem ich ponownie. Zamiast tego po prostu zamierzam usunąć mój komentarz dotyczący założenia, że ​​popełniłem błąd. Dziękuję za zwrócenie mi na to uwagi.
Howard Hinnant
2
@HowardHinnant, dziękuję bardzo, zawsze jest to cenny moment, kiedy pojawia się ten poziom uważności i wrażliwości, to takie odświeżające! (Oczywiście
skasuję

Odpowiedzi:

393

Powodem, dla którego Herb powiedział, jest to z powodu takich przypadków.

Powiedzmy, że mam funkcję, Aktóra wywołuje funkcję B, która wywołuje funkcję C. I Aprzechodzi sznurkiem do Bi do C. Anie wie lub nie dba o to C; wszystko Ao czym jest wiadomo B. To jest Cszczegół implementacji B.

Powiedzmy, że A jest zdefiniowane następująco:

void A()
{
  B("value");
}

Jeśli B i C zabiorą ciąg const&, to wygląda mniej więcej tak:

void B(const std::string &str)
{
  C(str);
}

void C(const std::string &str)
{
  //Do something with `str`. Does not store it.
}

Wszystko dobrze i dobrze. Po prostu podajesz wskazówki, nie kopiujesz, nie ruszasz się, wszyscy są szczęśliwi. Cbierze, const&ponieważ nie przechowuje ciągu. Po prostu go używa.

Teraz chcę wprowadzić jedną prostą zmianę: Ctrzeba gdzieś zapisać ciąg.

void C(const std::string &str)
{
  //Do something with `str`.
  m_str = str;
}

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 Aprzemija chwilowo; nie ma powodu, dla którego Cnależ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ę, Caby wziąć jego parametr według wartości, spowoduje to po prostu Bskopiowanie do tego parametru; Nic nie zyskuję.

Więc gdybym właśnie przekazał strwartość przez wszystkie funkcje, polegając na std::moveprzetasowaniu 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?

Nicol Bolas
źródło
2
Kiedy mówisz, że przejście na wartość jest droższe niż użycie referencji, to wciąż jest droższe o stałą kwotę (niezależnie od długości przenoszonego łańcucha), prawda?
Neil G
3
@NeilG: Czy rozumiesz, co oznacza „zależny od wdrożenia”? To, co mówisz, jest złe, ponieważ zależy to od tego, czy i w jaki sposób SSO jest realizowane.
ildjarn 19.04. O
17
@ildjarn: W analizie kolejności, jeśli najgorszy przypadek czegoś jest związany stałą, to wciąż jest to stały czas. Czy nie ma najdłuższego małego sznurka? Czy ten ciąg nie wymaga ciągłego czasu kopiowania? Czy kopiowanie wszystkich mniejszych ciągów zajmuje mniej czasu? Następnie kopiowanie ciągów dla małych ciągów jest „stałym czasem” w analizie kolejności - mimo że małe ciągi zajmują różną ilość czasu. Analiza porządku dotyczy zachowań asymptotycznych .
Neil G
8
@NeilG: Jasne, ale twoje pierwotne pytanie brzmiało: „ które jest wciąż droższe o stałą kwotę (niezależnie od długości przesuwanego łańcucha), prawda? ” Chodzi mi o to, że może być droższe przez inne stałe kwoty w zależności od długości łańcucha, który sumuje się jako „nie”.
ildjarn 19.04. O
13
Dlaczego ciąg miałby być movedod B do C w przypadku według wartości? Jeśli B jest, B(std::string b)a C jest C(std::string c), musimy albo wezwać C(std::move(b))B, albo bpozostać niezmienione (a więc „niewzruszone”) aż do wyjścia B. (Być może optymalizacji kompilator ruszy ciąg pod as-jeśli reguła, jeśli bnie jest stosowany po zakończeniu rozmowy, ale nie sądzę, istnieje silna gwarancja). To samo odnosi się do kopii strdo m_str. Nawet jeśli parametr funkcji został zainicjowany wartością rvalue, jest to wartość wewnątrz funkcji i std::movewymagane jest przejście z tej wartości.
Pixelchemist,
163

Czy minęły już dni przekazywania const std :: string & jako parametru?

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 przekazywanie std::stringwartoś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:

Jednak inval jest wciąż znacznie większy niż rozmiar odwołania (który zwykle jest implementowany jako wskaźnik). Wynika to z faktu, że std :: string ma różne komponenty, w tym wskaźnik do sterty i element członkowski 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ć?

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.

justin
źródło
3
W przypadku użycia stosu typowe ABI przekażą pojedyncze odniesienie w rejestrze bez użycia stosu.
ahcox
63

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:

bool foo1( const std::string v )
{
  return v.empty();
}
bool foo2( const std::string & v )
{
  return v.empty();
}

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, foo2osiągnie lepsze wyniki foo1, ponieważ foo1wykona głęboką kopię.

Na moim komputerze, używając g ++ 4.6.1, otrzymałem następujące wyniki:

  • zmienna przez odniesienie: 1000000000 iteracji -> upływ czasu: 2.25912 sek
  • zmienna wartość: 1000000000 iteracji -> czas, który upłynął: 27,2259 sek
  • literał przez odniesienie: 100000000 iteracji -> czas, który upłynął: 9.10319 sek
  • dosłownie według wartości: 100000000 iteracji -> upływ czasu: 8,62659 sek
BЈовић
źródło
4
Bardziej istotne jest to, co dzieje się wewnątrz funkcji: czy przy wywołaniu z referencją musiałby wykonać kopię wewnętrznie, którą można pominąć przy przekazywaniu wartości ?
lewo około
1
@leftaroundabout Tak, oczywiście. Moje założenie, że obie funkcje robią dokładnie to samo.
BЈовић
5
Nie o to mi chodzi. To, czy przekazywanie wartości lub odniesienie jest lepsze, zależy od tego, co robisz wewnątrz funkcji. W twoim przykładzie tak naprawdę nie używasz dużej części łańcucha, więc odwołanie jest oczywiście lepsze. Ale jeśli zadaniem funkcji byłoby umieszczenie łańcucha w jakiejś strukturze lub wykonanie, powiedzmy, algorytmu rekurencyjnego obejmującego wiele podziałów łańcucha, przekazywanie przez wartość mogłoby faktycznie zaoszczędzić trochę kopiowania, w porównaniu do przekazywania przez referencję. Nicol Bolas wyjaśnia to całkiem dobrze.
lewo około
3
Dla mnie „to zależy od tego, co robisz wewnątrz funkcji” jest złym projektem - ponieważ opierasz podpis funkcji na wewnętrznych elementach implementacji.
Hans Olsson,
1
Może to być literówka, ale dwa ostatnie dosłowne czasy mają 10 razy mniej pętli.
TankorSmash
54

Krótka odpowiedź: NIE! Długa odpowiedź:

  • Jeśli nie zmodyfikujesz ciągu (traktuj jako tylko do odczytu), przekaż go jako const ref&.
    ( const ref&oczywiście musi pozostać w zasięgu podczas wykonywania funkcji, która go używa)
  • Jeśli planujesz go zmodyfikować lub wiesz, że będzie on poza zasięgiem (wątki) , przekaż go jako value, nie kopiuj const ref&wnętrza funkcji.

Na stronie cpp-next.com pojawił się post zatytułowany „Chcesz prędkości przekazać wartość!” . TL; DR:

Wskazówka : Nie kopiuj argumentów funkcji. Zamiast tego przekaż je według wartości i pozwól kompilatorowi wykonać kopiowanie.

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 :

std::string function(const std::string& aString){
    auto vString(aString);
    vString.clear();
    return vString;
}

zrób to :

std::string function(std::string aString){
    aString.clear();
    return aString;
}

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.

CodeAngry
źródło
2
W niektórych przypadkach zaleca się przekazywanie przez odniesienie, ale wskazuje się na wytyczne, które zalecają zawsze przekazywanie według wartości.
Keith Thompson
3
@KeithThompson Nie kopiuj argumentów funkcji. Oznacza, że ​​nie kopiuje się 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.
CodeAngry
5
@KeithThompson Cytat z wytycznej (Nie kopiuj argumentów funkcji. Zamiast tego przekaż je wartością i pozwól kompilatorowi skopiować) z tej strony. Jeśli to nie jest wystarczająco jasne, nie mogę pomóc. Nie w pełni ufam kompilatorom, aby dokonywać najlepszych wyborów. Wolę bardzo jasno powiedzieć o moich zamiarach w sposobie definiowania argumentów funkcji. # 1 Jeśli jest tylko do odczytu, to jest 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ść, mijam ref&. # 4 Używam, pointers *jeśli argument jest opcjonalny, więc mogę to nullptrzrobić.
CodeAngry
11
Nie opowiadam się za tym, czy przekazać wartość czy referencję. Chodzi mi o to, że w niektórych przypadkach opowiadasz się za przekazywaniem referencji, ale powołujesz się (najwyraźniej na poparcie twojego stanowiska) na wytyczne, które zawsze zalecają przekazywanie wartości. Jeśli nie zgadzasz się z wytycznymi, możesz to powiedzieć i wyjaśnić dlaczego. (Linki do cpp-next.com nie działają dla mnie.)
Keith Thompson
4
@KeithThompson: Źle sparafrazowałeś wytyczne. Nie chodzi o „zawsze” przekazywanie wartości. Podsumowując, było to: „Jeśli utworzyłbyś kopię lokalną, użyj parametru pass by value, aby kompilator wykonał tę kopię za ciebie”. Nie mówi się o przekazywaniu wartości, gdy nie zamierzasz robić kopii.
Ben Voigt
43

O ile nie potrzebujesz kopii, nadal warto ją zabrać const &. Na przykład:

bool isprint(std::string const &s) {
    return all_of(begin(s),end(s),(bool(*)(char))isprint);
}

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.

bames53
źródło
2
Konstruktory przenoszenia są zwykle implementowane za pomocą noexcept, ale oczywiście konstruktory kopiujące nie są.
lewo około
25

Prawie.

W C ++ 17 mamy basic_string_view<?>, co sprowadza nas do jednego wąskiego przypadku użycia std::string const&parametrów.

Istnienie semantyki ruchu wyeliminowało jeden przypadek użycia std::string const&- jeśli planujesz przechowywać parametr, przyjęcie std::stringwartości według jest bardziej optymalne, ponieważ możesz movewyjść z parametru.

Jeśli ktoś nazwał twoją funkcję surowym C, "string"oznacza to, że std::stringprzydzielony jest tylko jeden bufor, w przeciwieństwie do dwóch w tym std::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::stringfunkcjonalność bez ryzyka alokacji. Surowy ciąg C może nawet zostać przekształcony w std::string_viewbez 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óre std::stringzapewniają. W praktyce jest to rzadki zestaw wymagań.

Jak - Adam Nevraumont
źródło
2
Doceniam tę odpowiedź - ale chcę podkreślić, że cierpi ona (podobnie jak wiele wysokiej jakości odpowiedzi) z powodu pewnych stronniczości specyficznych dla domeny. To znaczy: „W praktyce jest to rzadki zestaw wymagań”… z mojego własnego doświadczenia rozwojowego te ograniczenia - które wydają się rażąco wąskie dla autora - są spełnione, dosłownie przez cały czas. Warto to podkreślić.
fish2000
1
@ fish2000 Aby być jasnym, std::stringaby 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.
Robisz
@ Yakk-AdamNevraumont To jest sprawa YMMV - ale jest to częsty przypadek użycia, jeśli (powiedzmy) programujesz przeciwko POSIX lub innym interfejsom API, w których semantyka łańcucha C jest, jak najniższy wspólny mianownik. Powinienem naprawdę powiedzieć, że uwielbiam 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.
fish2000
1
@ fish2000 "„ Surowy ciąg C można nawet przekształcić w widok std :: string_view bez przypisania lub kopiowania znaków ”, co jest warte zapamiętania”. Rzeczywiście, ale pomija najlepszą część - w przypadku, gdy nieprzetworzony ciąg znaków jest dosłowny, nie wymaga nawet środowiska wykonawczego strlen () !
Don Hatch,
17

std::stringnie 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 invaljest 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, constreferencje mogą być lepszym sposobem.

Szczeniak
źródło
2
Ciekawe, że konstruktor kopii jest wystarczająco inteligentny, aby nie martwić się o logowanie jednokrotne, jeśli go nie używa. Prawdopodobnie poprawne, będę musiał sprawdzić, czy to prawda ;-)
Benj
3
@Benj: Stary komentarz, wiem, ale jeśli SSO jest wystarczająco małe, jego bezwarunkowe kopiowanie jest szybsze niż tworzenie gałęzi warunkowej. Na przykład 64 bajty to linia pamięci podręcznej, którą można skopiować w bardzo trywialny sposób. Prawdopodobnie 8 cykli lub mniej na x86_64.
Zan Lynx
Nawet jeśli jednokrotne logowanie nie jest kopiowane przez konstruktor kopiowania, 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.
cmaster
Aha, i zapomniałem mówić o przekazywaniu argumentów funkcji do rejestrów; to
obniżyłoby
16

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:

#include <iostream>

struct string
{
    string() {}
    string(const string&) {std::cout << "string(const string&)\n";}
    string& operator=(const string&) {std::cout << "string& operator=(const string&)\n";return *this;}
#if (__has_feature(cxx_rvalue_references))
    string(string&&) {std::cout << "string(string&&)\n";}
    string& operator=(string&&) {std::cout << "string& operator=(string&&)\n";return *this;}
#endif

};

#if PROCESS == 1

string
do_something(string inval)
{
    // do stuff
    return inval;
}

#elif PROCESS == 2

string
do_something(const string& inval)
{
    string return_val = inval;
    // do stuff
    return return_val; 
}

#if (__has_feature(cxx_rvalue_references))

string
do_something(string&& inval)
{
    // do stuff
    return std::move(inval);
}

#endif

#endif

string source() {return string();}

int main()
{
    std::cout << "do_something with lvalue:\n\n";
    string x;
    string t = do_something(x);
#if (__has_feature(cxx_rvalue_references))
    std::cout << "\ndo_something with xvalue:\n\n";
    string u = do_something(std::move(x));
#endif
    std::cout << "\ndo_something with prvalue:\n\n";
    string v = do_something(source());
}

Dla mnie ten wynik:

$ clang++ -std=c++11 -stdlib=libc++ -DPROCESS=1 test.cpp
$ a.out
do_something with lvalue:

string(const string&)
string(string&&)

do_something with xvalue:

string(string&&)
string(string&&)

do_something with prvalue:

string(string&&)
$ clang++ -std=c++11 -stdlib=libc++ -DPROCESS=2 test.cpp
$ a.out
do_something with lvalue:

string(const string&)

do_something with xvalue:

string(string&&)

do_something with prvalue:

string(string&&)

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:

+----+--------+--------+---------+
|    | lvalue | xvalue | prvalue |
+----+--------+--------+---------+
| p1 |  1/1   |  0/2   |   0/1   |
+----+--------+--------+---------+
| p2 |  1/0   |  0/1   |   0/1   |
+----+--------+--------+---------+

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.

Howard Hinnant
źródło
1
std :: string jest standardową klasą biblioteki. Już można go przenosić i kopiować. Nie rozumiem, jak to ma znaczenie. OP pyta więcej o wydajność przenoszenia kontra odniesienia , a nie o wydajność przenoszenia kontra kopiowania.
Nicol Bolas,
3
Ta odpowiedź liczy liczbę ruchów i kopii, które std :: string przejdzie w ramach projektu pass-by-value opisanego zarówno przez Herb, jak i Dave'a, w porównaniu do podania przez referencję z parą przeciążonych funkcji. Używam kodu OP w wersji demo, z wyjątkiem zastępowania ciągiem fikcyjnym, aby wykrzyczeć, gdy jest kopiowany / przenoszony.
Howard Hinnant,
Prawdopodobnie powinieneś zoptymalizować kod przed wykonaniem testów…
The Paramagnetic Croissant
3
@TheParamagneticCroissant: Czy uzyskałeś różne wyniki? Jeśli tak, to za pomocą jakiego kompilatora z jakim argumentem wiersza poleceń?
Howard Hinnant,
14

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ą promocji std::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.)

#include <string>
#include <iostream>
class WidgetBadRef {
public:
    WidgetBadRef(const std::string& s) : myStrRef(s)  // copy the reference...
    {}

    const std::string& myStrRef;    // might be a reference to a temporary (oops!)
};

class WidgetSafeCopy {
public:
    WidgetSafeCopy(const std::string& s) : myStrCopy(s)
            // constructor for string references; copy the string
    {std::cout << "const std::string& constructor\n";}

    WidgetSafeCopy(const char* cs) : myStrCopy(cs)
            // constructor for string literals (and char arrays);
            // for minor efficiency only;
            // create the std::string directly from the chars
    {std::cout << "const char * constructor\n";}

    const std::string myStrCopy;    // save a copy, not a reference!
};

int main() {
    WidgetBadRef w1("First string");
    WidgetSafeCopy w2("Second string"); // uses the const char* constructor, no temp string
    WidgetSafeCopy w3(w2.myStrCopy);    // uses the String reference constructor
    std::cout << w1.myStrRef << "\n";   // garbage out
    std::cout << w2.myStrCopy << "\n";  // OK
    std::cout << w3.myStrCopy << "\n";  // OK
}

WYNIK:

const char * constructor
const std::string& constructor

Second string
Second string
circlepi314
źródło
Jest to inny problem i WidgetBadRef nie musi mieć stałej i parametru, aby się nie udać. Pytanie brzmi, czy WidgetSafeCopy właśnie wziął parametr ciągu, czy byłby wolniejszy? (Myślę, że kopia tymczasowa dla członka jest z pewnością łatwiejsza do wykrycia)
Superfly Jon
Pamiętaj, że T&&nie zawsze jest to uniwersalne odniesienie; w rzeczywistości std::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.
Justin Time - Przywróć Monikę
@JustinTime: dziękuję; Usunąłem niepoprawne zdanie końcowe, twierdząc w efekcie, że std :: string && będzie uniwersalnym odniesieniem.
circlepi314 18.04.17
@ circlepi314 Nie ma za co. Jest to łatwa do pomieszania, czasami może być mylące, czy dana 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.
Justin Time - Przywróć Monikę
8

IMO wykorzystujące odwołanie do C ++ std::stringto 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:

  1. Jeśli napiszesz cały kod z zewnątrz do funkcji wewnętrznych, wiesz, co robi kod, możesz użyć odwołania const std::string &.
  2. Jeśli napiszesz kod biblioteki lub użyjesz mocno kodu biblioteki, do którego przekazywane są ciągi, prawdopodobnie zyskasz więcej w sensie globalnym, ufając std::stringzachowaniu konstruktora kopii.
digital_infinity
źródło
6

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.

slajd 24

Testy porównawcze pokazują, że przekazywanie std::stringwartoś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.

JDługosz
źródło
3

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, fktó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 fdostosowanych argumentów lvalue i rvalue. Aby zobaczyć, dlaczego tak jest, załóżmy, że fbierze się parametr wartości, gdzie Tjest jakiś możliwy do skopiowania i przeniesienia typ konstrukcyjny:

void f(T x) {
  T y{std::move(x)};
}

Wywołanie fz argumentem lvalue spowoduje wywołanie konstruktora kopii w celu skonstruowania xi konstruktora ruchu w celu skonstruowania y. Z drugiej strony, wywołanie fz argumentem rvalue spowoduje wywołanie konstruktora ruchu w celu skonstruowania xi innego konstruktora ruchu w celu skonstruowania y.

Zasadniczo optymalna implementacja fargumentów lvalue jest następująca:

void f(const T& x) {
  T y{x};
}

W takim przypadku do konstruowania wywoływany jest tylko jeden konstruktor kopii y. Optymalna implementacja fargumentów dla wartości jest znowu ogólnie następująca:

void f(T&& x) {
  T y{std::move(x)};
}

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ę, fktó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, fktóra zachowuje kopię swojego parametru, będzie miała postać:

void f(T x) {
  T y{...};
  ...
  y = std::move(x);
}

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:

void f(const T& x) {
  T y{...};
  ...
  y = x;
}

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, fktóra zachowuje kopię, ma postać:

void f(T&& x) {
  T y{...};
  ...
  y = std::move(x);
}

Zatem w tym przypadku tylko przypisanie ruchu. Przekazanie wartości do tej wersji fwymaga stałej referencji kosztuje tylko przypisanie zamiast przypisania ruchu. Mówiąc relatywnie, fpreferowana 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 fwypadek, gdyby zdecydowano się na przeciążenie kategorii wartości argumentu. Idealne przekazywanie ma tę wadę, że fstaje 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).

Ton van den Heuvel
źródło
Zobacz ustalenia Herb Suttera w mojej nowej odpowiedzi: rób to tylko wtedy, gdy poruszasz konstruktem , a nie przenosisz przypisanie.
JDługosz
1
@ JDługosz, dzięki za wskazówkę do przemówienia Herb, właśnie go obejrzałem i całkowicie poprawiłem swoją odpowiedź. Nie byłem świadomy (przeniesienia) przypisania porady.
Ton van den Heuvel
ta liczba i porady są teraz w dokumencie Standardowe wytyczne .
JDługosz
1

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

Erik Aronesty
źródło
@BenVoigt tak ... zamiast odlewania, powinno być możliwe do zmiany ... ale nie można zmienić członka STL na zmienny w klasie pochodnej.
Erik Aronesty,