Sind die Tage der Übergabe von const std :: string & als Parameter vorbei?


604

Ich hörte einen letzten Vortrag von Herb Sutter, der vorschlug , dass die Gründe passieren std::vectorund std::stringvon const &weitgehend verschwunden. Er schlug vor, eine Funktion wie die folgende zu schreiben:

std::string do_something ( std::string inval )
{
   std::string return_val;
   // ... do stuff ...
   return return_val;
}

Ich verstehe, dass der return_valWert an dem Punkt ist, an dem die Funktion zurückkehrt, und daher unter Verwendung der Verschiebungssemantik zurückgegeben werden kann, die sehr billig ist. Ist invaljedoch immer noch viel größer als die Größe einer Referenz (die normalerweise als Zeiger implementiert wird). Dies liegt daran, dass a std::stringverschiedene Komponenten enthält, einschließlich eines Zeigers in den Heap und eines Elements char[]für die Optimierung kurzer Zeichenfolgen. Daher scheint es mir immer noch eine gute Idee zu sein, als Referenz zu fungieren.

Kann jemand erklären, warum Herb das gesagt haben könnte?


89
Ich denke, die beste Antwort auf die Frage ist wahrscheinlich, Dave Abrahams Artikel darüber in C ++ Next zu lesen . Ich möchte hinzufügen, dass ich nichts sehe, was als nicht thematisch oder nicht konstruktiv eingestuft werden kann. Es ist eine klare Frage zur Programmierung, auf die es sachliche Antworten gibt.
Jerry Coffin

Faszinierend. Wenn Sie also trotzdem eine Kopie erstellen müssen, ist die Wertübergabe wahrscheinlich schneller als die Referenzübergabe.
Benj

3
@Sz. Ich bin sensibel für Fragen, die fälschlicherweise als Duplikate eingestuft und geschlossen werden. Ich erinnere mich nicht an die Details dieses Falls und habe sie nicht erneut überprüft. Stattdessen werde ich einfach meinen Kommentar unter der Annahme löschen, dass ich einen Fehler gemacht habe. Vielen Dank, dass Sie mich darauf aufmerksam gemacht haben.
Howard Hinnant

2
@HowardHinnant, vielen Dank, es ist immer ein kostbarer Moment, wenn man auf dieses Maß an Aufmerksamkeit und Sensibilität stößt. Es ist so erfrischend! (Ich werde meine dann natürlich löschen.)
Gr.

Antworten:


393

Der Grund, warum Herb sagte, was er sagte, sind solche Fälle.

Angenommen, ich habe Aeine Funktion B, die die Funktion aufruft , die die Funktion aufruft C. Und Ageht eine Schnur durch Bund in C. Aweiß nicht oder kümmert sich nicht darum C; alles Aweiß ist B. Das heißt, Cist ein Implementierungsdetail von B.

Angenommen, A ist wie folgt definiert:

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

Wenn B und C die Zeichenfolge vorbeikommen const&, sieht es ungefähr so ​​aus:

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

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

Alles schön und gut. Sie geben nur Hinweise weiter, kein Kopieren, keine Bewegung, alle sind glücklich. Cnimmt ein, const&weil es die Zeichenfolge nicht speichert. Es benutzt es einfach.

Jetzt möchte ich eine einfache Änderung vornehmen: CDie Zeichenfolge muss irgendwo gespeichert werden.

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

Hallo, Kopierkonstruktor und mögliche Speicherzuordnung (ignorieren Sie die Short String Optimization (SSO) ). Die Verschiebungssemantik von C ++ 11 soll es ermöglichen, unnötige Kopierkonstruktionen zu entfernen, oder? Und Ageht eine vorübergehende; es gibt keinen Grund , warum Cmüssen sollte kopiert die Daten. Es sollte nur mit dem fliehen, was ihm gegeben wurde.

Außer es kann nicht. Weil es eine braucht const&.

Wenn ich ändere C, um seinen Parameter nach Wert zu nehmen, führt dies nur dazu B, dass die Kopie in diesen Parameter ausgeführt wird. Ich gewinne nichts.

Wenn ich also nur stralle Funktionen als Wert durchlaufen hätte und mich darauf verlassen würde std::move, die Daten zu mischen, hätten wir dieses Problem nicht. Wenn jemand daran festhalten will, kann er es. Wenn nicht, na ja.

Ist es teurer? Ja; Der Übergang zu einem Wert ist teurer als die Verwendung von Referenzen. Ist es billiger als die Kopie? Nicht für kleine Saiten mit SSO. Lohnt es sich zu tun?

Dies hängt von Ihrem Anwendungsfall ab. Wie sehr hassen Sie Speicherzuweisungen?


2
Wenn Sie sagen, dass das Verschieben in einen Wert teurer ist als das Verwenden von Referenzen, ist dies immer noch um einen konstanten Betrag (unabhängig von der Länge der zu verschiebenden Zeichenfolge) teurer, oder?
Neil G

3
@NeilG: Verstehst du was "implementierungsabhängig" bedeutet? Was Sie sagen, ist falsch, weil es davon abhängt, ob und wie SSO implementiert ist.
ildjarn

17
@ildjarn: Wenn in der Ordnungsanalyse der schlimmste Fall von etwas durch eine Konstante gebunden ist, ist es immer noch eine konstante Zeit. Gibt es nicht eine längste kleine Saite? Dauert das Kopieren dieser Zeichenfolge nicht konstant lange? Nehmen alle kleineren Zeichenfolgen nicht weniger Zeit zum Kopieren in Anspruch? Dann ist das Kopieren von Zeichenfolgen für kleine Zeichenfolgen in der Auftragsanalyse "konstante Zeit" - obwohl kleine Zeichenfolgen unterschiedlich viel Zeit zum Kopieren benötigen. Die Ordnungsanalyse befasst sich mit asymptotischem Verhalten .
Neil G

8
@NeilG: Sicher, aber Ihre ursprüngliche Frage war: " Das ist immer noch um einen konstanten Betrag teurer (unabhängig von der Länge der zu bewegenden Saite), oder? " Der Punkt, den ich versuche, ist, dass es um verschiedene teurer sein könnte konstante Beträge abhängig von der Länge der Zeichenfolge, die als "Nein" zusammengefasst wird.
ildjarn

13
Warum sollte die Zeichenfolge movedim Fall nach Wert von B nach C sein? Wenn B ist B(std::string b)und C ist, müssen C(std::string c)wir entweder C(std::move(b))B aufrufen oder müssen bunverändert bleiben (also 'unbewegt von'), bis wir beenden B. (Vielleicht verschiebt ein optimierender Compiler die Zeichenfolge unter die Als-ob-Regel, wenn bsie nach dem Aufruf nicht verwendet wird, aber ich glaube nicht, dass es eine starke Garantie gibt.) Gleiches gilt für die Kopie von strto m_str. Selbst wenn ein Funktionsparameter mit einem r-Wert initialisiert wurde, ist er ein l-Wert innerhalb der Funktion und std::movemuss von diesem l-Wert abweichen.
Pixelchemist

163

Sind die Tage der Übergabe von const std :: string & als Parameter vorbei?

Nein . Viele Leute (einschließlich Dave Abrahams) nehmen diesen Rat über den Bereich hinaus an, für den er gilt, und vereinfachen ihn, um ihn auf alle std::string Parameter anzuwenden. Die ständige Übergabe std::stringvon Werten ist keine "Best Practice" für alle beliebigen Parameter und Anwendungen, da diese optimiert werden Vorträge / Artikel konzentrieren sich nur auf eine begrenzte Anzahl von Fällen .

Wenn Sie einen Wert zurückgeben, den Parameter mutieren oder den Wert übernehmen, kann das Übergeben eines Werts teures Kopieren sparen und syntaktischen Komfort bieten.

Wie immer spart das Übergeben einer const-Referenz viel Kopieren, wenn Sie keine Kopie benötigen .

Nun zum konkreten Beispiel:

Inval ist jedoch immer noch viel größer als die Größe einer Referenz (die normalerweise als Zeiger implementiert wird). Dies liegt daran, dass ein std :: string verschiedene Komponenten enthält, einschließlich eines Zeigers in den Heap und eines Mitglieds char [] für die Optimierung kurzer Strings. Daher scheint es mir immer noch eine gute Idee zu sein, als Referenz zu fungieren. Kann jemand erklären, warum Herb das gesagt haben könnte?

Wenn die Stapelgröße ein Problem darstellt (und vorausgesetzt, dass dies nicht inline / optimiert ist), return_val+ inval> return_val- IOW, kann die maximale Stapelnutzung reduziert werden, indem hier ein Wert übergeben wird (Hinweis: Übervereinfachung von ABIs). In der Zwischenzeit kann das Übergeben einer const-Referenz die Optimierungen deaktivieren. Der Hauptgrund hierbei ist nicht, das Stapelwachstum zu vermeiden, sondern sicherzustellen, dass die Optimierung dort durchgeführt werden kann, wo sie anwendbar ist .

Die Tage des Vergehens der Konstantenreferenz sind noch nicht vorbei - die Regeln sind nur komplizierter als früher. Wenn die Leistung wichtig ist, sollten Sie überlegen, wie Sie diese Typen übergeben, basierend auf den Details, die Sie in Ihren Implementierungen verwenden.


3
Bei der Stapelverwendung übergeben typische ABIs eine einzelne Referenz in einem Register ohne Stapelverwendung.
Ahcox

63

Dies hängt stark von der Implementierung des Compilers ab.

Dies hängt jedoch auch davon ab, was Sie verwenden.

Betrachten wir die nächsten Funktionen:

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

Diese Funktionen werden in einer separaten Kompilierungseinheit implementiert, um Inlining zu vermeiden. Dann:
1. Wenn Sie diesen beiden Funktionen ein Literal übergeben, werden Sie keinen großen Unterschied in der Leistung feststellen. In beiden Fällen muss ein String-Objekt erstellt werden.
2. Wenn Sie ein anderes std :: string-Objekt übergeben, foo2wird es eine Outperformance erzielen foo1, da foo1eine tiefe Kopie erstellt wird.

Auf meinem PC mit g ++ 4.6.1 habe ich folgende Ergebnisse erhalten:

  • Variable durch Referenz: 1000000000 Iterationen -> verstrichene Zeit: 2.25912 Sek
  • variabel nach Wert: 1000000000 Iterationen -> verstrichene Zeit: 27,2259 Sek
  • Literal durch Bezugnahme: 100000000 Iterationen -> verstrichene Zeit: 9.10319 Sek
  • Literal nach Wert: 100000000 Iterationen -> verstrichene Zeit: 8,62659 Sek

4
Was relevanter ist, ist, was innerhalb der Funktion passiert : Würde es, wenn es mit einer Referenz aufgerufen wird, eine interne Kopie erstellen müssen, die beim Übergeben von Werten weggelassen werden kann ?
links um den

1
@leftaroundabout Ja, natürlich. Meine Annahme, dass beide Funktionen genau dasselbe tun.
BЈовић

5
Das ist nicht mein Punkt. Ob die Übergabe nach Wert oder Referenz besser ist, hängt davon ab, was Sie in der Funktion tun. In Ihrem Beispiel verwenden Sie nicht viel von dem Zeichenfolgenobjekt, daher ist die Referenz offensichtlich besser. Wenn die Aufgabe der Funktion jedoch darin besteht, die Zeichenfolge in einer Struktur zu platzieren oder beispielsweise einen rekursiven Algorithmus auszuführen, der mehrere Teilungen der Zeichenfolge umfasst, kann das Übergeben von Werten im Vergleich zum Übergeben als Referenz tatsächlich etwas Kopieren sparen . Nicol Bolas erklärt es ganz gut.
links um den

3
Für mich ist "es hängt davon ab, was Sie innerhalb der Funktion tun" ein schlechtes Design - da Sie die Signatur der Funktion auf den Interna der Implementierung basieren.
Hans Olsson

1
Könnte ein Tippfehler sein, aber die letzten beiden wörtlichen Timings haben 10x weniger Schleifen.
TankorSmash

54

Kurze Antwort: NEIN! Lange Antwort:

  • Wenn Sie die Zeichenfolge nicht ändern (behandeln ist schreibgeschützt), übergeben Sie sie als const ref&.
    (Das muss const ref&offensichtlich im Rahmen bleiben, während die Funktion, die es verwendet, ausgeführt wird.)
  • Wenn Sie vorhaben, es zu ändern, oder wenn Sie wissen, dass es außerhalb des Gültigkeitsbereichs (Threads) liegt , übergeben Sie es als value, kopieren Sie nicht das const ref&Innere Ihres Funktionskörpers.

Auf cpp-next.com gab es einen Beitrag mit dem Titel "Willst du Geschwindigkeit, pass by value!" . Die TL; DR:

Richtlinie : Kopieren Sie Ihre Funktionsargumente nicht. Übergeben Sie sie stattdessen als Wert und lassen Sie den Compiler das Kopieren durchführen.

ÜBERSETZUNG von ^

Kopieren Sie Ihre Funktionsargumente nicht --- bedeutet: Wenn Sie den Argumentwert durch Kopieren in eine interne Variable ändern möchten, verwenden Sie stattdessen einfach ein Wertargument .

Also mach das nicht :

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

mach das :

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

Wenn Sie den Argumentwert in Ihrem Funktionskörper ändern müssen.

Sie müssen nur wissen, wie Sie das Argument im Funktionskörper verwenden möchten. Schreibgeschützt oder NICHT ... und wenn es im Geltungsbereich bleibt.


2
In einigen Fällen empfehlen Sie, als Referenz zu übergeben, verweisen jedoch auf eine Richtlinie, in der empfohlen wird, immer nach Wert zu übergeben.
Keith Thompson

3
@KeithThompson Kopieren Sie Ihre Funktionsargumente nicht. Das heißt, kopieren Sie das nicht const ref&in eine interne Variable, um es zu ändern. Wenn Sie es ändern müssen ... machen Sie den Parameter zu einem Wert. Es ist ziemlich klar für mein nicht englischsprachiges Ich.
CodeAngry

5
@KeithThompson Die Leitlinie Zitat (Do not copy Ihre Argumente Funktion. Stattdessen sie nach Wert übergeben und lassen Sie den Compiler das Kopieren zu tun.) Wird von dieser Seite kopiert. Wenn das nicht klar genug ist, kann ich nicht helfen. Ich vertraue Compilern nicht voll und ganz, um die besten Entscheidungen zu treffen. Ich möchte lieber sehr klar über meine Absichten bei der Definition von Funktionsargumenten sein. # 1 Wenn es schreibgeschützt ist, ist es a const ref&. # 2 Wenn ich es schreiben muss oder weiß, dass es außerhalb des Geltungsbereichs liegt ... verwende ich einen Wert. # 3 Wenn ich den ursprünglichen Wert ändern muss, gehe ich vorbei ref&. # 4 Ich benutze, pointers *wenn ein Argument optional ist, damit ich es kann nullptr.
CodeAngry

11
Ich bin nicht auf der Seite der Frage, ob ich nach Wert oder nach Referenz gehen soll. Mein Punkt ist, dass Sie in einigen Fällen die Übergabe als Referenz befürworten, dann aber (scheinbar um Ihre Position zu unterstützen) eine Richtlinie zitieren, die empfiehlt, immer nach Wert zu übergeben. Wenn Sie mit der Richtlinie nicht einverstanden sind, möchten Sie dies möglicherweise sagen und erklären, warum. (Die Links zu cpp-next.com funktionieren nicht für mich.)
Keith Thompson

4
@ KeithThompson: Sie haben die Richtlinie falsch umschrieben. Es ist nicht "immer" Wert zu übergeben. Zusammenfassend war es "Wenn Sie eine lokale Kopie erstellt hätten, verwenden Sie pass by value, damit der Compiler diese Kopie für Sie ausführt." Es heißt nicht, Pass-by-Value zu verwenden, wenn Sie keine Kopie erstellen wollten.
Ben Voigt

43

Wenn Sie keine Kopie benötigen, ist es immer noch vernünftig, sie zu nehmen const &. Zum Beispiel:

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

Wenn Sie dies ändern, um die Zeichenfolge nach Wert zu nehmen, wird der Parameter am Ende verschoben oder kopiert, und das ist nicht erforderlich. Das Kopieren / Verschieben ist wahrscheinlich nicht nur teurer, sondern führt auch zu einem neuen potenziellen Fehler. Das Kopieren / Verschieben kann eine Ausnahme auslösen (z. B. kann die Zuordnung während des Kopierens fehlschlagen), während das Verweisen auf einen vorhandenen Wert dies nicht kann.

Wenn Sie noch eine Kopie benötigen dann vorbei und durch den Wert der Rückkehr ist in der Regel (immer?) , Um die beste Option. Tatsächlich würde ich mich in C ++ 03 im Allgemeinen nicht darum kümmern, es sei denn, Sie stellen fest, dass zusätzliche Kopien tatsächlich ein Leistungsproblem verursachen. Copy Elision scheint auf modernen Compilern ziemlich zuverlässig zu sein. Ich denke, die Skepsis und das Bestehen der Leute darauf, dass Sie Ihre Tabelle der Compiler-Unterstützung für RVO überprüfen müssen, ist heutzutage größtenteils veraltet.


Kurz gesagt, C ++ 11 ändert in dieser Hinsicht nichts wirklich, außer Menschen, die der Kopierentscheidung nicht vertrauten.


2
Verschiebungskonstruktoren werden normalerweise mit implementiert noexcept, Kopierkonstruktoren jedoch offensichtlich nicht.
links um den

25

Fast.

In C ++ 17 haben wir basic_string_view<?>, was uns auf einen engen Anwendungsfall für std::string const&Parameter reduziert.

Das Vorhandensein einer Verschiebungssemantik hat einen Anwendungsfall für beseitigt std::string const&: Wenn Sie den Parameter speichern möchten, std::stringist es optimaler, einen By-Wert zu verwenden, da Sie moveden Parameter verlassen können.

Wenn jemand Ihre Funktion mit einem rohen C aufgerufen hat, "string"bedeutet dies, dass immer nur ein std::stringPuffer zugewiesen wird, im Gegensatz zu zwei std::string const&.

Wenn Sie jedoch keine Kopie erstellen möchten, std::string const&ist das Mitnehmen in C ++ 14 weiterhin hilfreich.

Mit std::string_view, wie Sie die C-Stil erwartet werden so lange nicht Zeichenfolge an eine API sagte passing '\0'Puffer terminierte Charakter, können Sie effizienter erhalten std::stringwie Funktionalität , ohne eine Zuordnung zu riskieren. Eine rohe C-Zeichenfolge kann sogar std::string_viewohne Zuweisung oder Kopieren von Zeichen in eine Zeichenfolge umgewandelt werden.

Zu diesem Zeitpunkt wird verwendet, std::string const&wenn Sie den Datengroßhandel nicht kopieren und ihn an eine API im C-Stil weiterleiten, die einen nullterminierten Puffer erwartet, und Sie die übergeordneten Zeichenfolgenfunktionen benötigen, die std::stringbereitgestellt werden. In der Praxis ist dies eine seltene Reihe von Anforderungen.


2
Ich schätze diese Antwort - aber ich möchte darauf hinweisen, dass sie (wie viele hochwertige Antworten) unter einer gewissen domänenspezifischen Verzerrung leidet. Das heißt: „In der Praxis ist dies eine seltene Reihe von Anforderungen.“… Nach meiner eigenen Entwicklungserfahrung werden diese Einschränkungen - die für den Autor ungewöhnlich eng erscheinen - buchstäblich immer erfüllt. Es lohnt sich darauf hinzuweisen.
fish2000

1
@ fish2000 Um klar zu sein, um std::stringzu dominieren, braucht man nicht nur einige dieser Anforderungen, sondern alle . Ich gebe zu, dass einer oder sogar zwei davon üblich sind. Vielleicht brauchen Sie normalerweise alle 3 (zum Beispiel analysieren Sie ein String-Argument, um auszuwählen, an welche C-API Sie es im Großhandel übergeben
möchten

@ Yakk-AdamNevraumont Es ist eine YMMV-Sache - aber es ist ein häufiger Anwendungsfall, wenn Sie (sagen wir) gegen POSIX oder andere APIs programmieren, bei denen die C-String-Semantik wie der kleinste gemeinsame Nenner ist. Ich sollte wirklich sagen, dass ich es liebe std::string_view- wie Sie betonen: „Eine rohe C-Zeichenfolge kann sogar ohne Zuweisung oder Kopieren von Zeichen in eine std :: string_view umgewandelt werden“, was für diejenigen, die C ++ im Kontext von verwenden, eine Erinnerung wert ist eine solche API-Nutzung in der Tat.
fish2000

1
@ fish2000 "'Eine rohe C-Zeichenfolge kann sogar ohne Zuweisung oder Kopieren von Zeichen in eine std :: string_view umgewandelt werden', woran man sich erinnern sollte". In der Tat, aber es lässt das Beste aus - für den Fall, dass die rohe Zeichenfolge ein Zeichenfolgenliteral ist, ist nicht einmal eine Laufzeit-Zeichenfolge strlen () erforderlich !
Don Hatch

17

std::stringist kein Plain Old Data (POD) , und seine Rohgröße ist nicht die relevanteste Sache überhaupt. Wenn Sie beispielsweise eine Zeichenfolge übergeben, die über der Länge von SSO liegt und auf dem Heap zugewiesen ist, würde ich erwarten, dass der Kopierkonstruktor den SSO-Speicher nicht kopiert.

Der Grund, warum dies empfohlen wird, liegt darin, dass invales aus dem Argumentausdruck aufgebaut ist und daher immer entsprechend verschoben oder kopiert wird. Es gibt keinen Leistungsverlust, vorausgesetzt, Sie benötigen das Eigentum an dem Argument. Wenn Sie dies nicht tun, könnte eine constReferenz immer noch der bessere Weg sein.


2
Interessanter Punkt, dass der Kopierkonstruktor intelligent genug ist, um sich keine Sorgen um das SSO zu machen, wenn es es nicht verwendet. Wahrscheinlich richtig, ich muss überprüfen, ob das stimmt ;-)
Benj

3
@Benj: Alter Kommentar, den ich kenne, aber wenn SSO klein genug ist, ist das bedingungslose Kopieren schneller als das Ausführen einer bedingten Verzweigung. Zum Beispiel sind 64 Bytes eine Cache-Zeile und können in sehr trivialer Zeit kopiert werden. Wahrscheinlich 8 Zyklen oder weniger auf x86_64.
Zan Lynx

Selbst wenn das SSO nicht vom Kopierkonstruktor kopiert wird, sind std::string<>es 32 Bytes, die vom Stapel zugewiesen werden, von denen 16 initialisiert werden müssen. Vergleichen Sie dies mit nur 8 Bytes, die als Referenz zugewiesen und initialisiert wurden: Es ist doppelt so viel CPU-Arbeit und nimmt viermal so viel Cache-Speicherplatz ein, der anderen Daten nicht zur Verfügung steht.
cmaster - wieder herstellen Monica

Oh, und ich habe vergessen, über das Übergeben von Funktionsargumenten in Registern zu sprechen. das würde die Stapelverwendung der Referenz für den letzten Angerufenen auf Null senken ...
cmaster - Monica

16

Ich habe die Antwort von dieser Frage hier kopiert / eingefügt und die Namen und die Schreibweise geändert, um sie an diese Frage anzupassen.

Hier ist Code, um zu messen, was gefragt wird:

#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());
}

Für mich ergibt dies:

$ 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&&)

Die folgende Tabelle fasst meine Ergebnisse zusammen (mit clang -std = c ++ 11). Die erste Zahl ist die Anzahl der Kopierkonstruktionen und die zweite Zahl ist die Anzahl der Verschiebungskonstruktionen:

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

Die Pass-by-Value-Lösung erfordert nur eine Überladung, kostet jedoch eine zusätzliche Bewegungskonstruktion, wenn l-Werte und x-Werte übergeben werden. Dies kann für eine bestimmte Situation akzeptabel sein oder auch nicht. Beide Lösungen haben Vor- und Nachteile.


1
std :: string ist eine Standardbibliotheksklasse. Es ist bereits sowohl beweglich als auch kopierbar. Ich sehe nicht, wie wichtig das ist. Das OP fragt mehr nach der Leistung von Verschieben vs. Referenzen , nicht nach der Leistung von Verschieben vs. Kopieren.
Nicol Bolas

3
Diese Antwort zählt die Anzahl der Züge und Kopien, die ein std :: string unter dem von Herb und Dave beschriebenen Pass-by-Value-Design durchläuft, im Vergleich zum Übergeben als Referenz mit einem Paar überladener Funktionen. Ich verwende den OP-Code in der Demo, außer dass ich ihn durch eine Dummy-Zeichenfolge ersetze, um zu schreien, wenn er kopiert / verschoben wird.
Howard Hinnant

Sie sollten wahrscheinlich den Code optimieren, bevor Sie die Tests durchführen…
Das paramagnetische Croissant

3
@TheParamagneticCroissant: Haben Sie unterschiedliche Ergebnisse erzielt? Wenn ja, welchen Compiler mit welchen Befehlszeilenargumenten verwenden?
Howard Hinnant

14

Herb Sutter ist zusammen mit Bjarne Stroustroup immer noch in der Empfehlung const std::string&, einen Parametertyp zu empfehlen . Siehe https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Rf-in .

In keiner der anderen Antworten wird eine Falle erwähnt: Wenn Sie ein String-Literal an einen const std::string&Parameter übergeben, wird ein Verweis auf eine temporäre Zeichenfolge übergeben, die im laufenden Betrieb erstellt wurde, um die Zeichen des Literals zu speichern. Wenn Sie diese Referenz dann speichern, ist sie ungültig, sobald die Zuordnung der temporären Zeichenfolge aufgehoben wird. Um sicher zu gehen, müssen Sie eine Kopie speichern , nicht die Referenz. Das Problem ergibt sich aus der Tatsache, dass Zeichenfolgenliterale const char[N]Typen sind, für die eine Heraufstufung erforderlich ist std::string.

Der folgende Code zeigt die pitfall und die Abhilfe, zusammen mit einer geringen Effizienz Option - mit einem Überlastung const char*Verfahren, wie bei beschriebenen gibt es einen Weg , um eine Stringliteral als Referenz in C ++ passieren .

(Hinweis: Sutter & Stroustroup empfehlen, dass Sie, wenn Sie eine Kopie des Strings behalten, auch eine überladene Funktion mit einem && -Parameter und std :: move () bereitstellen.)

#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
}

AUSGABE:

const char * constructor
const std::string& constructor

Second string
Second string

Dies ist ein anderes Problem, und WidgetBadRef muss keinen const & -Parameter haben, um einen Fehler zu machen. Die Frage ist, ob WidgetSafeCopy nur einen Zeichenfolgenparameter verwendet, wäre es langsamer? (Ich denke, die Kopie vorübergehend für Mitglied ist sicherlich leichter zu erkennen)
Superfly Jon

Beachten Sie, dass dies T&&nicht immer eine universelle Referenz ist. Tatsächlich std::string&&wird es immer eine Wertreferenz und niemals eine universelle Referenz sein, da kein Typabzug durchgeführt wird . Somit widerspricht der Rat von Stroustroup & Sutter nicht dem von Meyers.
Justin Time - Stellen Sie Monica am

@ JustinTime: Danke; Ich habe den falschen letzten Satz entfernt und behauptet, dass std :: string && eine universelle Referenz wäre.
circlepi314

@ circlepi314 Gern geschehen. Es ist eine einfache Verwechslung, es kann manchmal verwirrend sein, ob eine gegebene T&&eine abgeleitete universelle Referenz oder eine nicht abgeleitete r-Wert-Referenz ist; Es wäre wahrscheinlich klarer gewesen, wenn sie ein anderes Symbol für universelle Referenzen eingeführt hätten (z. B. &&&als Kombination von &und &&), aber das würde wahrscheinlich nur albern aussehen.
Justin Time - Stellen Sie Monica am

8

IMO mit der C ++ - Referenz für std::stringist eine schnelle und kurze lokale Optimierung, während die Verwendung der Übergabe von Werten eine bessere globale Optimierung sein könnte (oder nicht).

Die Antwort lautet also: Es hängt von den Umständen ab:

  1. Wenn Sie den gesamten Code von außen nach innen schreiben, wissen Sie, was der Code tut, und können die Referenz verwenden const std::string &.
  2. Wenn Sie den Bibliothekscode schreiben oder stark Bibliothekscode verwenden, in dem Zeichenfolgen übergeben werden, gewinnen Sie wahrscheinlich mehr im globalen Sinne, wenn Sie dem std::stringVerhalten des Kopierkonstruktors vertrauen .

6

Siehe "Herb Sutter" Zurück zu den Grundlagen! Grundlagen des modernen C ++ - Stils . Unter anderem bespricht er die in der Vergangenheit gegebenen Ratschläge zur Parameterübergabe und neue Ideen, die mit C ++ 11 einhergehen, und befasst sich speziell mit dem Idee, Zeichenfolgen nach Wert zu übergeben.

Folie 24

Die Benchmarks zeigen, dass das Übergeben von std::strings nach Wert in Fällen, in denen die Funktion es trotzdem kopiert, erheblich langsamer sein kann!

Dies liegt daran, dass Sie es zwingen, immer eine vollständige Kopie zu erstellen (und dann an Ort und Stelle zu verschieben), während die const&Version die alte Zeichenfolge aktualisiert, die möglicherweise den bereits zugewiesenen Puffer wiederverwendet.

Siehe seine Folie 27: Für "Set" -Funktionen ist Option 1 dieselbe wie immer. Option 2 fügt eine Überlastung für die r-Wert-Referenz hinzu, dies führt jedoch zu einer kombinatorischen Explosion, wenn mehrere Parameter vorhanden sind.

Nur für "sink" -Parameter, bei denen eine Zeichenfolge erstellt werden muss (ohne dass der vorhandene Wert geändert wird), ist der Trick zum Übergeben von Werten gültig. Das heißt, Konstruktoren, in denen der Parameter das Mitglied des übereinstimmenden Typs direkt initialisiert.

Wenn Sie sehen möchten, wie tief Sie sich Sorgen machen können, schauen Sie sich die Präsentation von Nicolai Josuttis an und wünschen Sie viel Glück damit ( "Perfekt - Fertig!" N-mal, nachdem Sie einen Fehler in der vorherigen Version gefunden haben. Waren Sie schon einmal dort?)


Dies ist in den Standardrichtlinien auch als ⧺F.15 zusammengefasst .


3

Wie @ JDługosz in den Kommentaren hervorhebt, gibt Herb in einem anderen (späteren?) Vortrag weitere Ratschläge, siehe ungefähr hier: https://youtu.be/xnqTKD8uD64?t=54m50s .

Sein Rat läuft darauf hinaus, nur Wertparameter für eine Funktion zu verwenden f, die sogenannte Senkenargumente verwendet, vorausgesetzt, Sie verschieben das Konstrukt aus diesen Senkenargumenten.

Dieser allgemeine Ansatz erhöht nur den Overhead eines Verschiebungskonstruktors sowohl für lvalue- als auch für rvalue-Argumente im Vergleich zu einer optimalen Implementierung von fauf lvalue- bzw. rvalue-Argumente zugeschnittenen Argumenten. Um zu sehen, warum dies der Fall ist, nehmen wir an, dass fein Wertparameter verwendet wird, bei dem Tes sich um einen konstruierbaren Typ zum Kopieren und Verschieben handelt:

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

Das Aufrufen fmit einem lvalue-Argument führt dazu, dass ein Kopierkonstruktor zum Konstruieren xund ein Verschiebungskonstruktor zum Konstruieren aufgerufen werden y. fWenn Sie dagegen mit einem rvalue-Argument aufrufen, wird ein Verschiebungskonstruktor zum Konstruieren xund ein anderer Verschiebungskonstruktor zum Konstruieren aufgerufen y.

Im Allgemeinen ist die optimale Implementierung von ffor lvalue-Argumenten wie folgt:

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

In diesem Fall wird nur ein Kopierkonstruktor zum Konstruieren aufgerufen y. Die optimale Implementierung von ffor rvalue-Argumenten ist im Allgemeinen wiederum wie folgt:

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

In diesem Fall wird nur ein Verschiebungskonstruktor zum Konstruieren aufgerufen y.

Ein vernünftiger Kompromiss besteht also darin, einen Wertparameter zu verwenden und einen zusätzlichen Zugkonstruktoraufruf für lvalue- oder rvalue-Argumente in Bezug auf die optimale Implementierung durchzuführen, was auch der Ratschlag in Herbs Vortrag ist.

Wie @ JDługosz in den Kommentaren hervorhob, ist die Übergabe von Werten nur für Funktionen sinnvoll, die ein Objekt aus dem sink-Argument erstellen. Wenn Sie eine Funktion haben f, die ihr Argument kopiert, hat der Pass-by-Value-Ansatz mehr Overhead als ein allgemeiner Pass-by-Const-Referenzansatz. Der Pass-by-Value-Ansatz für eine Funktion f, die eine Kopie ihres Parameters beibehält, hat folgende Form:

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

In diesem Fall gibt es eine Kopierkonstruktion und eine Verschiebungszuweisung für ein lvalue-Argument sowie eine Verschiebungskonstruktion und eine Verschiebungszuweisung für ein rvalue-Argument. Der optimalste Fall für ein lvalue-Argument ist:

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

Dies läuft nur auf eine Zuweisung hinaus, die möglicherweise viel billiger ist als der Kopierkonstruktor plus Verschiebungszuweisung, die für den Pass-by-Value-Ansatz erforderlich ist. Der Grund dafür ist, dass die Zuweisung möglicherweise vorhandenen zugewiesenen Speicher in wiederverwendet yund daher (De-) Zuweisungen verhindert, während der Kopierkonstruktor normalerweise Speicher zuweist.

Für ein rvalue-Argument hat die optimalste Implementierung f, bei der eine Kopie erhalten bleibt, die folgende Form:

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

Also nur eine Zugzuordnung in diesem Fall. Das Übergeben eines r-Werts an die Version, für fdie eine konstante Referenz erforderlich ist, kostet nur eine Zuweisung anstelle einer Verschiebungszuweisung. Relativ gesehen fist es daher vorzuziehen, in diesem Fall eine konstante Referenz als allgemeine Implementierung zu verwenden.

Im Allgemeinen müssen Sie für eine optimale Implementierung eine perfekte Weiterleitung durchführen, wie im Vortrag gezeigt. Der Nachteil ist eine kombinatorische Explosion der Anzahl der erforderlichen Überladungen, abhängig von der Anzahl der Parameter für den fFall, dass Sie sich für eine Überlastung der Wertekategorie des Arguments entscheiden. Perfekte Weiterleitung hat den Nachteil, dass sie fzu einer Vorlagenfunktion wird, die verhindert, dass sie virtuell wird, und zu wesentlich komplexerem Code führt, wenn Sie ihn zu 100% richtig machen möchten (Einzelheiten finden Sie im Vortrag).


Siehe die Ergebnisse von Herb Sutter in meiner neuen Antwort: Tun Sie dies nur, wenn Sie das Konstrukt verschieben , nicht die Zuweisung.
JDługosz

1
@ JDługosz, danke für den Hinweis auf Herbs Vortrag, ich habe ihn mir gerade angesehen und meine Antwort komplett überarbeitet. Mir war der (Umzugs-) Zuweisungsrat nicht bekannt.
Ton van den Heuvel

Diese Zahl und dieser Rat sind jetzt im Dokument mit den Standardrichtlinien enthalten .
JDługosz

1

Das Problem ist, dass "const" ein nicht granulares Qualifikationsmerkmal ist. Was normalerweise mit "const string ref" gemeint ist, ist "diese Zeichenfolge nicht ändern", nicht "den Referenzzähler nicht ändern". In C ++ gibt es einfach keine Möglichkeit zu sagen, welche Mitglieder "const" sind. Sie sind entweder alle oder keiner von ihnen.

Um dieses Sprachproblem zu umgehen, könnte STL "C ()" in Ihrem Beispiel trotzdem erlauben, eine verschiebungssemantische Kopie zu erstellen , und die "const" in Bezug auf die Referenzanzahl pflichtbewusst ignorieren (veränderlich). Solange es gut spezifiziert war, wäre dies in Ordnung.

Da STL dies nicht tut, habe ich eine Version eines Strings, der const_casts <> den Referenzzähler entfernt (keine Möglichkeit, etwas in einer Klassenhierarchie rückwirkend veränderbar zu machen), und - siehe da - Sie können cmstrings frei als const-Referenzen übergeben. und machen Sie Kopien davon in tiefen Funktionen, den ganzen Tag, ohne Lecks oder Probleme.

Da C ++ hier keine "abgeleitete Klasse const granularity" bietet, ist es die beste Lösung, eine gute Spezifikation zu schreiben und ein glänzendes neues "const movable string" -Objekt (cmstring) zu erstellen.


@BenVoigt yep ... anstatt wegzuwerfen, sollte es veränderbar sein ... aber Sie können ein STL-Mitglied in einer abgeleiteten Klasse nicht in veränderbar ändern.
Erik Aronesty
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.