Ich hörte einen letzten Vortrag von Herb Sutter, der vorschlug , dass die Gründe passieren std::vector
und std::string
von 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_val
Wert 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 inval
jedoch immer noch viel größer als die Größe einer Referenz (die normalerweise als Zeiger implementiert wird). Dies liegt daran, dass a std::string
verschiedene 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?
Antworten:
Der Grund, warum Herb sagte, was er sagte, sind solche Fälle.
Angenommen, ich habe
A
eine FunktionB
, die die Funktion aufruft , die die Funktion aufruftC
. UndA
geht eine Schnur durchB
und inC
.A
weiß nicht oder kümmert sich nicht darumC
; allesA
weiß istB
. Das heißt,C
ist ein Implementierungsdetail vonB
.Angenommen, A ist wie folgt definiert:
Wenn B und C die Zeichenfolge vorbeikommen
const&
, sieht es ungefähr so aus:Alles schön und gut. Sie geben nur Hinweise weiter, kein Kopieren, keine Bewegung, alle sind glücklich.
C
nimmt ein,const&
weil es die Zeichenfolge nicht speichert. Es benutzt es einfach.Jetzt möchte ich eine einfache Änderung vornehmen:
C
Die Zeichenfolge muss irgendwo gespeichert werden.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
A
geht eine vorübergehende; es gibt keinen Grund , warumC
mü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 dazuB
, dass die Kopie in diesen Parameter ausgeführt wird. Ich gewinne nichts.Wenn ich also nur
str
alle Funktionen als Wert durchlaufen hätte und mich darauf verlassen würdestd::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?
quelle
moved
im Fall nach Wert von B nach C sein? Wenn B istB(std::string b)
und C ist, müssenC(std::string c)
wir entwederC(std::move(b))
B aufrufen oder müssenb
unverändert bleiben (also 'unbewegt von'), bis wir beendenB
. (Vielleicht verschiebt ein optimierender Compiler die Zeichenfolge unter die Als-ob-Regel, wennb
sie nach dem Aufruf nicht verwendet wird, aber ich glaube nicht, dass es eine starke Garantie gibt.) Gleiches gilt für die Kopie vonstr
tom_str
. Selbst wenn ein Funktionsparameter mit einem r-Wert initialisiert wurde, ist er ein l-Wert innerhalb der Funktion undstd::move
muss von diesem l-Wert abweichen.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 Übergabestd::string
von 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:
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.
quelle
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:
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,
foo2
wird es eine Outperformance erzielenfoo1
, dafoo1
eine tiefe Kopie erstellt wird.Auf meinem PC mit g ++ 4.6.1 habe ich folgende Ergebnisse erhalten:
quelle
Kurze Antwort: NEIN! Lange Antwort:
const ref&
.(Das muss
const ref&
offensichtlich im Rahmen bleiben, während die Funktion, die es verwendet, ausgeführt wird.)value
, kopieren Sie nicht dasconst 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:
Ü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 :
mach das :
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.
quelle
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.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 vorbeiref&
. # 4 Ich benutze,pointers *
wenn ein Argument optional ist, damit ich es kannnullptr
.Wenn Sie keine Kopie benötigen, ist es immer noch vernünftig, sie zu nehmen
const &
. Zum Beispiel: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.
quelle
noexcept
, Kopierkonstruktoren jedoch offensichtlich nicht.Fast.
In C ++ 17 haben wir
basic_string_view<?>
, was uns auf einen engen Anwendungsfall fürstd::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::string
ist es optimaler, einen By-Wert zu verwenden, da Siemove
den Parameter verlassen können.Wenn jemand Ihre Funktion mit einem rohen C aufgerufen hat,
"string"
bedeutet dies, dass immer nur einstd::string
Puffer zugewiesen wird, im Gegensatz zu zweistd::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 erhaltenstd::string
wie Funktionalität , ohne eine Zuordnung zu riskieren. Eine rohe C-Zeichenfolge kann sogarstd::string_view
ohne 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, diestd::string
bereitgestellt werden. In der Praxis ist dies eine seltene Reihe von Anforderungen.quelle
std::string
zu 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 übergebenstd::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.std::string
ist 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
inval
es 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 eineconst
Referenz immer noch der bessere Weg sein.quelle
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.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:
Für mich ergibt dies:
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:
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.
quelle
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 Zeichenfolgenliteraleconst char[N]
Typen sind, für die eine Heraufstufung erforderlich iststd::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.)
AUSGABE:
quelle
T&&
nicht immer eine universelle Referenz ist. Tatsächlichstd::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.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.IMO mit der C ++ - Referenz für
std::string
ist 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:
const std::string &
.std::string
Verhalten des Kopierkonstruktors vertrauen .quelle
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.
Die Benchmarks zeigen, dass das Übergeben von
std::string
s 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 .
quelle
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
f
auf lvalue- bzw. rvalue-Argumente zugeschnittenen Argumenten. Um zu sehen, warum dies der Fall ist, nehmen wir an, dassf
ein Wertparameter verwendet wird, bei demT
es sich um einen konstruierbaren Typ zum Kopieren und Verschieben handelt:Das Aufrufen
f
mit einem lvalue-Argument führt dazu, dass ein Kopierkonstruktor zum Konstruierenx
und ein Verschiebungskonstruktor zum Konstruieren aufgerufen werdeny
.f
Wenn Sie dagegen mit einem rvalue-Argument aufrufen, wird ein Verschiebungskonstruktor zum Konstruierenx
und ein anderer Verschiebungskonstruktor zum Konstruieren aufgerufeny
.Im Allgemeinen ist die optimale Implementierung von
f
for lvalue-Argumenten wie folgt:In diesem Fall wird nur ein Kopierkonstruktor zum Konstruieren aufgerufen
y
. Die optimale Implementierung vonf
for rvalue-Argumenten ist im Allgemeinen wiederum wie folgt: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 Funktionf
, die eine Kopie ihres Parameters beibehält, hat folgende Form: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:
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
y
und 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:Also nur eine Zugzuordnung in diesem Fall. Das Übergeben eines r-Werts an die Version, für
f
die eine konstante Referenz erforderlich ist, kostet nur eine Zuweisung anstelle einer Verschiebungszuweisung. Relativ gesehenf
ist 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
f
Fall, dass Sie sich für eine Überlastung der Wertekategorie des Arguments entscheiden. Perfekte Weiterleitung hat den Nachteil, dass sief
zu 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).quelle
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.
quelle