Ist es in C ++ besser, einen Wert oder eine konstante Referenz zu übergeben?

Antworten:

203

Es verwendet , um beste Praxis in der Regel empfohlen werden 1 zu verwenden Pass von const ref für alle Typen , mit Ausnahme von eingebauten Typen ( char, int, double, etc.), für Iteratoren und Funktionsobjekte (Lambda - Ausdrücke, Klassen die sich aus std::*_function).

Dies galt insbesondere vor der Existenz der Bewegungssemantik . Der Grund ist einfach: Wenn Sie einen Wert übergeben haben, musste eine Kopie des Objekts erstellt werden. Mit Ausnahme sehr kleiner Objekte ist dies immer teurer als das Übergeben einer Referenz.

Mit C ++ 11 haben wir die Bewegungssemantik gewonnen . Kurz gesagt, die Bewegungssemantik ermöglicht es, dass ein Objekt in einigen Fällen "nach Wert" übergeben werden kann, ohne es zu kopieren. Dies ist insbesondere dann der Fall, wenn das Objekt, das Sie übergeben, ein r-Wert ist .

An sich ist das Bewegen eines Objekts immer noch mindestens so teuer wie das Übergeben als Referenz. In vielen Fällen kopiert eine Funktion ein Objekt jedoch trotzdem intern - dh sie übernimmt das Eigentum an dem Argument. 2

In diesen Situationen haben wir den folgenden (vereinfachten) Kompromiss:

  1. Wir können das Objekt als Referenz übergeben und dann intern kopieren.
  2. Wir können das Objekt als Wert übergeben.

"Wert übergeben" bewirkt weiterhin, dass das Objekt kopiert wird, es sei denn, das Objekt ist ein r-Wert. Im Falle eines r-Werts kann das Objekt stattdessen verschoben werden, so dass der zweite Fall plötzlich nicht mehr "kopieren, dann verschieben", sondern "verschieben, dann (möglicherweise) wieder verschieben" lautet.

Bei großen Objekten, die geeignete Verschiebungskonstruktoren implementieren (wie Vektoren, Zeichenfolgen usw.), ist der zweite Fall dann weitaus effizienter als der erste. Daher wird empfohlen, den Wert " Übergeben" zu verwenden, wenn die Funktion den Besitz des Arguments übernimmt und der Objekttyp ein effizientes Verschieben unterstützt .


Eine historische Anmerkung:

Tatsächlich sollte jeder moderne Compiler in der Lage sein, herauszufinden, wann das Übergeben von Werten teuer ist, und den Aufruf implizit so zu konvertieren, dass er nach Möglichkeit eine const ref verwendet.

In der Theorie. In der Praxis können Compiler dies nicht immer ändern, ohne die binäre Schnittstelle der Funktion zu beschädigen. In einigen speziellen Fällen (wenn die Funktion inline ist) wird die Kopie tatsächlich entfernt, wenn der Compiler herausfinden kann, dass das ursprüngliche Objekt durch die Aktionen in der Funktion nicht geändert wird.

Im Allgemeinen kann der Compiler dies jedoch nicht feststellen, und das Aufkommen der Verschiebungssemantik in C ++ hat diese Optimierung viel weniger relevant gemacht.


1 ZB in Scott Meyers, Effektives C ++ .

2 Dies gilt insbesondere häufig für Objektkonstruktoren, die Argumente verwenden und intern speichern, um Teil des Status des konstruierten Objekts zu sein.

Konrad Rudolph
quelle
hmmm ... ich bin mir nicht sicher, ob es sich lohnt, an ref vorbeizukommen. double-s
sergtk
3
Boost hilft hier wie immer. boost.org/doc/libs/1_37_0/libs/utility/call_traits.htm enthält Vorlagen, mit denen automatisch herausgefunden werden kann, wann ein Typ ein integrierter Typ ist (nützlich für Vorlagen, bei denen Sie dies manchmal nicht so einfach wissen können).
CesarB
13
Diese Antwort verfehlt einen wichtigen Punkt. Um ein Schneiden zu vermeiden, müssen Sie als Referenz übergeben (const oder anderweitig). Siehe stackoverflow.com/questions/274626/…
ChrisN
6
@ Chris: richtig. Ich habe den ganzen Teil des Polymorphismus weggelassen, weil das eine völlig andere Semantik ist. Ich glaube, das OP (semantisch) bedeutete "durch Wert" Argument übergeben. Wenn andere Semantiken erforderlich sind, stellt sich die Frage nicht einmal von selbst.
Konrad Rudolph
98

Edit: Neuer Artikel von Dave Abrahams auf cpp-next:

Willst du Geschwindigkeit? Wert übergeben.


Das Übergeben eines Werts für Strukturen, bei denen das Kopieren billig ist, hat den zusätzlichen Vorteil, dass der Compiler davon ausgehen kann, dass die Objekte keinen Alias ​​haben (nicht dieselben Objekte sind). Bei Verwendung der Referenzübergabe kann der Compiler dies nicht immer annehmen. Einfaches Beispiel:

foo * f;

void bar(foo g) {
    g.i = 10;
    f->i = 2;
    g.i += 5;
}

Der Compiler kann es optimieren

g.i = 15;
f->i = 2;

da es weiß, dass f und g nicht den gleichen Ort haben. Wenn g eine Referenz wäre (foo &), hätte der Compiler das nicht annehmen können. da gi dann durch f-> i aliasiert werden könnte und einen Wert von 7 haben müsste, müsste der Compiler den neuen Wert von gi erneut aus dem Speicher abrufen.

Weitere praktische Regeln finden Sie im Artikel zum Verschieben von Konstruktoren (empfohlene Lektüre).

  • Wenn die Funktion beabsichtigt, das Argument als Nebeneffekt zu ändern, nehmen Sie es als nicht konstante Referenz.
  • Wenn die Funktion ihr Argument nicht ändert und das Argument vom primitiven Typ ist, nehmen Sie es als Wert.
  • Andernfalls nehmen Sie es als const-Referenz, außer in den folgenden Fällen
    • Wenn die Funktion dann ohnehin eine Kopie der const-Referenz erstellen müsste, nehmen Sie sie als Wert.

"Primitiv" bedeutet im Grunde genommen kleine Datentypen, die einige Bytes lang und nicht polymorph (Iteratoren, Funktionsobjekte usw.) oder teuer zu kopieren sind. In diesem Artikel gibt es eine andere Regel. Die Idee ist, dass man manchmal eine Kopie erstellen möchte (falls das Argument nicht geändert werden kann) und manchmal nicht (falls man das Argument selbst in der Funktion verwenden möchte, wenn das Argument ohnehin temporär war , zum Beispiel). Das Papier erklärt ausführlich, wie das gemacht werden kann. In C ++ 1x kann diese Technik nativ mit Sprachunterstützung verwendet werden. Bis dahin würde ich mit den oben genannten Regeln gehen.

Beispiele: Um einen String in Großbuchstaben zu schreiben und die Großbuchstabenversion zurückzugeben, sollte immer der Wert übergeben werden: Man muss sowieso eine Kopie davon erstellen (man konnte die const-Referenz nicht direkt ändern) - also machen Sie es besser so transparent wie möglich den Anrufer und erstellen Sie diese Kopie frühzeitig, damit der Anrufer so viel wie möglich optimieren kann - wie in diesem Dokument beschrieben:

my::string uppercase(my::string s) { /* change s and return it */ }

Wenn Sie den Parameter jedoch ohnehin nicht ändern müssen, beziehen Sie sich auf const:

bool all_uppercase(my::string const& s) { 
    /* check to see whether any character is uppercase */
}

Wenn Sie jedoch den Zweck des Parameters haben, etwas in das Argument zu schreiben, übergeben Sie es als nicht konstante Referenz

bool try_parse(T text, my::string &out) {
    /* try to parse, write result into out */
}
Johannes Schaub - litb
quelle
Ich fand deine Regeln gut, aber ich bin mir nicht sicher über den ersten Teil, in dem du darüber sprichst, sie nicht zu bestehen, da ein Schiedsrichter sie beschleunigen würde. Ja sicher, aber etwas nicht als Schiedsrichter zu bestehen, nur weil es optimistisch ist, macht überhaupt keinen Sinn. Wenn Sie das übergebene Stapelobjekt ändern möchten, tun Sie dies mit ref. Wenn Sie dies nicht tun, übergeben Sie es als Wert. Wenn Sie es nicht ändern möchten, übergeben Sie es als const-ref. Die Optimierung, die mit dem Pass-by-Value einhergeht, sollte keine Rolle spielen, da Sie beim Übergeben als Referenz andere Dinge gewinnen. Ich verstehe das "Geschwindigkeit wollen?" nicht. sice wenn du wo diese op durchführen willst, würdest du sowieso nach Wert gehen ..
chikuba
Johannes: Ich habe diesen Artikel geliebt , als ich ihn gelesen habe, aber ich war enttäuscht, als ich ihn ausprobiert habe. Dieser Code ist sowohl bei GCC als auch bei MSVC fehlgeschlagen. Habe ich etwas verpasst oder funktioniert es in der Praxis nicht?
user541686
Ich glaube nicht, dass ich damit einverstanden bin, dass Sie, wenn Sie trotzdem eine Kopie erstellen möchten, diese als Wert übergeben (anstelle von const ref) und dann verschieben. Betrachten Sie es so, was ist effizienter, eine Kopie und ein Umzug (Sie können sogar 2 Kopien haben, wenn Sie es weitergeben) oder nur eine Kopie? Ja, es gibt einige Sonderfälle auf beiden Seiten, aber wenn Ihre Daten trotzdem nicht verschoben werden können (z. B. ein POD mit Tonnen von Ganzzahlen), sind keine zusätzlichen Kopien erforderlich.
Ion Todirel
2
Mehrdad, nicht sicher, was Sie erwartet haben, aber der Code funktioniert wie erwartet
Ion Todirel
Ich würde die Notwendigkeit des Kopierens in Betracht ziehen, um den Compiler davon zu überzeugen, dass sich die Typen nicht mit einem Sprachmangel überschneiden. Ich würde lieber GCCs verwenden __restrict__(die auch für Referenzen geeignet sind ), als übermäßige Kopien zu erstellen. Schade, dass Standard-C ++ das restrictSchlüsselwort von C99 nicht übernommen hat .
Ruslan
12

Kommt auf den Typ an. Sie fügen den kleinen Aufwand hinzu, eine Referenz und eine Dereferenzierung vornehmen zu müssen. Bei Typen mit einer Größe, die gleich oder kleiner als Zeiger ist und den Standardkopierer verwendet, ist es wahrscheinlich schneller, einen Wert zu übergeben.

Lou Franco
quelle
Bei nicht nativen Typen können Sie (abhängig davon, wie gut der Compiler den Code optimiert) eine Leistungssteigerung erzielen, indem Sie konstante Referenzen anstelle von nur Referenzen verwenden.
ABl.
9

Wie bereits erwähnt, hängt dies vom Typ ab. Für integrierte Datentypen ist es am besten, den Wert zu übergeben. Sogar einige sehr kleine Strukturen, wie z. B. ein Int-Paar, können durch Übergeben von Werten eine bessere Leistung erzielen.

In diesem Beispiel wird davon ausgegangen, dass Sie einen ganzzahligen Wert haben und diesen an eine andere Routine übergeben möchten. Wenn dieser Wert für die Speicherung in einem Register optimiert wurde, muss er, wenn Sie ihn als Referenz übergeben möchten, zuerst im Speicher gespeichert und dann mit einem Zeiger auf diesen Speicher auf dem Stapel abgelegt werden, um den Aufruf auszuführen. Wenn es als Wert übergeben wurde, ist nur das Register erforderlich, das auf den Stapel geschoben wird. (Die Details sind etwas komplizierter als bei verschiedenen aufrufenden Systemen und CPUs).

Wenn Sie eine Vorlagenprogrammierung durchführen, müssen Sie normalerweise immer const ref übergeben, da Sie die übergebenen Typen nicht kennen. Das Übergeben von Strafen für das Übergeben von Wertwerten ist viel schlimmer als das Übergeben eines integrierten Typs von const ref.

Torlack
quelle
Hinweis zur Terminologie: Eine Struktur mit einer Million Ints ist immer noch ein "POD-Typ". Möglicherweise meinen Sie "für integrierte Typen ist es am besten, den Wert zu übergeben".
Steve Jessop
6

Dies ist, was ich normalerweise arbeite, wenn ich die Schnittstelle einer Nicht-Template-Funktion entwerfe:

  1. Übergeben Sie den Wert, wenn die Funktion den Parameter nicht ändern möchte und der Wert billig zu kopieren ist (int, double, float, char, bool usw.). Beachten Sie, dass std :: string, std :: vector und der Rest der Container in der Standardbibliothek sind NICHT)

  2. Übergeben Sie den const-Zeiger, wenn das Kopieren des Werts teuer ist und die Funktion den angegebenen Wert nicht ändern möchte und NULL ein Wert ist, den die Funktion verarbeitet.

  3. Übergeben Sie einen nicht konstanten Zeiger, wenn das Kopieren des Werts teuer ist und die Funktion den angegebenen Wert ändern möchte und NULL ein Wert ist, den die Funktion verarbeitet.

  4. Übergeben Sie die const-Referenz, wenn das Kopieren des Werts teuer ist und die Funktion den angegebenen Wert nicht ändern möchte und NULL kein gültiger Wert wäre, wenn stattdessen ein Zeiger verwendet würde.

  5. Übergeben Sie eine nicht konstante Referenz, wenn das Kopieren des Werts teuer ist und die Funktion den angegebenen Wert ändern möchte und NULL kein gültiger Wert wäre, wenn stattdessen ein Zeiger verwendet würde.

Martin G.
quelle
Fügen Sie std::optionaldem Bild hinzu und Sie benötigen keine Zeiger mehr.
Violette Giraffe
5

Klingt so, als hättest du deine Antwort bekommen. Das Übergeben von Werten ist teuer, bietet Ihnen jedoch eine Kopie, mit der Sie bei Bedarf arbeiten können.

GeekyMonkey
quelle
Ich bin mir nicht sicher, warum dies abgelehnt wurde? Das macht für mich Sinn. Wenn Sie den aktuell gespeicherten Wert benötigen, übergeben Sie den Wert. Wenn nicht, übergeben Sie die Referenz.
Totty
4
Es ist völlig typabhängig. Das Ausführen eines POD-Typs (Plain Old Data) als Referenz kann tatsächlich die Leistung verringern, indem mehr Speicherzugriffe verursacht werden.
Torlack
1
Offensichtlich spart die Übergabe von int als Referenz nichts! Ich denke, die Frage impliziert Dinge, die größer als ein Zeiger sind.
GeekyMonkey
4
Es ist nicht so offensichtlich, ich habe viel Code von Leuten gesehen, die nicht wirklich verstehen, wie Computer funktionieren, indem sie einfache Dinge von const ref übergeben, weil ihnen gesagt wurde, dass dies das Beste ist, was zu tun ist.
Torlack
4

In der Regel ist es besser, die Konstantenreferenz zu übergeben. Wenn Sie Ihr Funktionsargument jedoch lokal ändern müssen, sollten Sie die Übergabe von Werten verwenden. Für einige Grundtypen ist die Leistung im Allgemeinen gleich, sowohl für die Übergabe nach Wert als auch als Referenz. Tatsächlich wird die Referenz intern durch einen Zeiger dargestellt. Aus diesem Grund können Sie beispielsweise erwarten, dass für den Zeiger beide Übergaben in Bezug auf die Leistung gleich sind oder dass die Übergabe durch Werte aufgrund unnötiger Dereferenzierung schneller sein kann.

sergtk
quelle
Wenn Sie die Kopie des Parameters durch den Angerufenen ändern müssen, können Sie eine Kopie im aufgerufenen Code erstellen, anstatt den Wert zu übergeben. IMO sollten Sie die API im Allgemeinen nicht basierend auf einem solchen Implementierungsdetail auswählen: Die Quelle des aufrufenden Codes ist in beiden Fällen dieselbe, der Objektcode jedoch nicht.
Steve Jessop
Wenn Sie einen Wert übergeben, wird eine Kopie erstellt. Und IMO spielt es keine Rolle, auf welche Weise Sie eine Kopie erstellen: über Argumente, die als Wert übergeben werden, oder lokal - dies betrifft C ++. Aber vom Design her stimme ich Ihnen zu. Aber ich beschreibe hier nur C ++ - Funktionen und berühre kein Design.
Sergtk
1

Als Faustregel gilt der Wert für Nicht-Klassentypen und die Konstantenreferenz für Klassen. Wenn eine Klasse wirklich klein ist, ist es wahrscheinlich besser, den Wert zu übergeben, aber der Unterschied ist minimal. Was Sie wirklich vermeiden möchten, ist, eine gigantische Klasse nach Wert zu übergeben und alles zu duplizieren - dies macht einen großen Unterschied, wenn Sie beispielsweise einen std :: -Vektor mit einigen Elementen übergeben.

Peter
quelle
Mein Verständnis ist, dass std::vectortatsächlich seine Elemente auf dem Heap zugeordnet werden und das Vektorobjekt selbst nie wächst. Oh, Moment mal. Wenn die Operation jedoch bewirkt, dass eine Kopie des Vektors erstellt wird, werden tatsächlich alle Elemente dupliziert. Das wäre schlecht.
Steven Lu
1
Ja, das habe ich mir gedacht. sizeof(std::vector<int>)ist konstant, aber wenn Sie es als Wert übergeben, wird der Inhalt auch ohne Compiler-Cleverness kopiert.
Peter
1

Wert für kleine Typen übergeben.

Übergeben Sie const-Referenzen für große Typen (die Definition von big kann zwischen den Computern variieren), ABER übergeben Sie in C ++ 11 den Wert, wenn Sie die Daten verbrauchen möchten, da Sie die Verschiebungssemantik ausnutzen können. Beispielsweise:

class Person {
 public:
  Person(std::string name) : name_(std::move(name)) {}
 private:
  std::string name_;
};

Jetzt würde der aufrufende Code reichen:

Person p(std::string("Albert"));

Und nur ein Objekt wird erstellt und direkt in das Mitglied name_in der Klasse verschoben Person. Wenn Sie an einer const-Referenz vorbeikommen, muss eine Kopie erstellt werden, in die Sie sie einfügen können name_.

Germán Diago
quelle
-5

Einfacher Unterschied: - In der Funktion haben wir Eingabe- und Ausgabeparameter. Wenn also Ihr übergebener Eingabe- und Ausgabeparameter identisch ist, verwenden Sie call by reference, andernfalls, wenn sich Eingabe- und Ausgabeparameter unterscheiden, ist es besser, call by value zu verwenden.

Beispiel void amount(int account , int deposit , int total )

Eingabeparameter: Konto, Einzahlungsausgabeparameter: gesamt

Eingabe und Ausgabe ist unterschiedliche Verwendung Anruf von Vaule

  1. void amount(int total , int deposit )

Input Total Deposit Output Total

Dhirendra Sengar
quelle