Übergabe von Parametern als Referenz und als Wert. Übergabe von Parametern nach Wert und Referenz. Übergabe von Parametern an eine Funktion nach Wert

Parameter können auf eine der folgenden Arten an eine Funktion übergeben werden:

Beim Übergeben von Argumenten nach Wert erstellt der Compiler eine temporäre Kopie des zu übergebenden Objekts und platziert sie in einem Bereich des Stapelspeichers, der zum Speichern lokaler Objekte vorgesehen ist. Die aufgerufene Funktion bearbeitet diese Kopie, ohne das Originalobjekt zu beeinflussen. Prototypen von Funktionen, die Argumente als Wert annehmen, stellen den Typ des Objekts und nicht seine Adresse als Parameter bereit. Zum Beispiel die Funktion

int GetMax(int, int);

Nimmt zwei ganzzahlige Argumente als Wert an.

Wenn es für die Funktion erforderlich ist, das ursprüngliche Objekt zu ändern, wird die Übergabe von Parametern per Referenz verwendet. In diesem Fall wird der Funktion nicht das Objekt selbst übergeben, sondern nur seine Adresse. Daher wirken sich alle Änderungen im Funktionskörper der per Referenz übergebenen Argumente auf das Objekt aus. Wenn man bedenkt, dass eine Funktion nur einen einzigen Wert zurückgeben kann, ist die Verwendung der Adresse eines Objekts eine sehr effiziente Möglichkeit, große Datenmengen zu verarbeiten. Da außerdem die Adresse und nicht das Objekt selbst übertragen wird, wird Stapelspeicher erheblich eingespart.

Zeiger verwenden.

Bei der referenziellen Übergabesyntax wird ein Verweis auf den Objekttyp als Argument verwendet. Zum Beispiel die Funktion

double Glue(long& var1, int& var2);

erhält zwei Referenzen auf Variablen vom Typ long und int. Bei der Übergabe eines Referenzparameters an eine Funktion übergibt der Compiler automatisch die Adresse der als Argument angegebenen Variablen an die Funktion. Es ist nicht erforderlich, vor einem Argument in einem Funktionsaufruf ein kaufmännisches Und zu platzieren. Für die vorherige Funktion sieht ein Aufruf, der Parameter per Referenz übergibt, beispielsweise so aus:

Kleber(var1, var2);

Nachfolgend finden Sie ein Beispiel für einen Funktionsprototyp bei der Übergabe von Parametern über einen Zeiger:

void SetNumber(int*, long*);

Darüber hinaus können Funktionen nicht nur den Wert einer Variablen zurückgeben, sondern auch einen Zeiger oder eine Referenz darauf. Zum Beispiel Funktionen, deren Prototyp ist:

*int Count(int); &int Erhöhung();

Gibt einen Zeiger bzw. eine Referenz auf eine Ganzzahlvariable vom Typ int zurück. Beachten Sie, dass die Rückgabe einer Referenz oder eines Zeigers von einer Funktion zu Problemen führen kann, wenn die referenzierte Variable außerhalb des Gültigkeitsbereichs liegt. Zum Beispiel,

Die Effizienz der Übergabe der Adresse eines Objekts anstelle der Variablen selbst macht sich auch in der Arbeitsgeschwindigkeit bemerkbar, insbesondere wenn große Objekte, insbesondere Arrays, verwendet werden (wird später besprochen).

Wenn Sie ein ziemlich großes Objekt an eine Funktion übergeben müssen, dessen Änderung jedoch nicht beabsichtigt ist, wird in der Praxis die Übergabe eines konstanten Zeigers verwendet. Bei dieser Art von Aufruf wird das Schlüsselwort const verwendet, beispielsweise function

const int* FName(int* const Number)

akzeptiert einen Zeiger auf ein konstantes Objekt vom Typ int und gibt ihn zurück. Jeder Versuch, ein solches Objekt im Hauptteil der aufgerufenen Funktion zu ändern, erzeugt eine Fehlermeldung vom Compiler. Schauen wir uns ein Beispiel an, das die Verwendung konstanter Zeiger veranschaulicht.

#enthalten

int* const call(int* const);

int X = 13; int* pX = call(pX);

int* const call(int* const x)

//*x++; II Sie können das Objekt nicht ändern! x zurückgeben;

Anstelle der oben genannten Const-Pointer-Syntax können Sie bei der Parameterübergabe alternativ auch Const-Referenzen verwenden, zum Beispiel:

const int& FName (const int& Nummer)

haben die gleiche Bedeutung wie konstante Zeiger.

#enthalten

const int& call(const int& x)

// Sie können ein Objekt nicht ändern!

Wenn eine Funktion eine andere aufruft, erfolgt die Kommunikation zwischen ihnen normalerweise über die Verwendung globaler Variablen, der Rückgabewerte und Parameter der aufgerufenen Funktion.

Programmiersprachen bieten im Wesentlichen zwei Möglichkeiten, Parameter an ein Unterprogramm zu übergeben. Der erste ist Wert übergeben. Bei seiner Verwendung wird der Wert des Aktualparameters (Arguments) in den Formalparameter des Unterprogramms kopiert. In diesem Fall haben Änderungen am Formalparameter keinen Einfluss auf das eigentliche Argument.

Die zweite Möglichkeit, Parameter an ein Unterprogramm zu übergeben, ist als Referenz übergeben. Bei Verwendung wird die Adresse des eigentlichen Arguments in den Formalparameter kopiert. Dies bedeutet, dass Änderungen am Wert des formalen Parameters im Gegensatz zur Wertübergabe genau die gleichen Änderungen am Wert des tatsächlichen Arguments zur Folge haben.

In der Sprache C gibt es nur eine Möglichkeit, tatsächliche und formale Parameter zu vergleichen – die Übergabe als Wert (die Übergabe von Parametern als Referenz ist in C++ möglich). In Pascal gibt es eine Wertübergabe und eine Referenzübergabe. Es gibt andere Methoden (in Fortran – Kopieren-Wiederherstellen, in Algol – Übertragung nach Namen).

Die Übergabe von Werten ist die einfachste Möglichkeit, Parameter zu übergeben. In diesem Fall werden die tatsächlichen Parameter berechnet und die resultierenden Werte an die aufgerufene Prozedur übergeben.

Die Pass-by-Value-Methode wird folgendermaßen implementiert:

    Der formale Parameter wird als lokale Variable behandelt, daher wird im Aktivierungsdatensatz der aufgerufenen Funktion Speicher dafür zugewiesen, d. h. auf Stapel;

    Die aufrufende Funktion wertet die tatsächlichen Parameter aus und legt ihre Werte im für formale Parameter zugewiesenen Speicher ab.

19.2. Übergabe von Parametern an Funktionen in der Sprache C

In der Sprache C werden Argumente beim Aufruf einer Funktion immer als Wert übergeben, d. h. Auf dem Stapel wird Platz für die formalen Parameter einer Funktion reserviert, und die Werte der tatsächlichen Argumente werden beim Aufruf in diesen zugewiesenen Platz geschrieben. Die Funktion nutzt diese dann und kann diese Werte auf dem Stack verändern. Wenn die Funktion jedoch beendet wird, gehen die geänderten Werte verloren. Eine aufgerufene Funktion kann die Werte von Variablen, die beim Aufruf der Funktion als tatsächliche Argumente angegeben wurden, nicht ändern.

void f(int k) ( k = -k; ) void main() ( int i = 1; f(i); printf("i = %d\n", i); // Ergebnis: i = 1 )

Notiz! Wir müssen bedenken, dass eine Kopie des Arguments an die Funktion übergeben wird. Was innerhalb der Funktion passiert, hat keinen Einfluss auf den Wert der Variablen, die beim Aufruf als Argument verwendet wurde. Genau aus diesem Grund können Sie beim Aufruf einer Funktion übrigens Konstanten und Ausdrücke als tatsächliche Argumente angeben und nicht nur Variablen.

19.3. Übergabe von Zeigern auf Funktionen

Was passiert, wenn die Funktion den Wert eines tatsächlichen Parameters ändern muss? Der naheliegendste, aber nicht beste Weg besteht darin, einen solchen Parameter durch eine globale Variable zu ersetzen. Der Nachteil besteht darin, dass beim Aufrufen von Funktionen aufgrund unerklärter Nebenwirkungen ein erhöhtes Fehlerrisiko besteht.

Bei Bedarf können mit der Funktion die ihr übergebenen Argumente geändert werden. In diesem Fall muss der aufgerufenen Funktion nicht der Wert des Arguments, sondern der Wert seiner Adresse als Argument übergeben werden, d. h. Zeiger. Da der Funktion die Adresse des Arguments übergeben wird, kann ihr interner Code den Wert dieses Arguments ändern.

Der Zeiger wird wie jedes andere Argument an die Funktion übergeben – als Wert. Es ist klar, dass bei der Übergabe einer Adresse der Parameter als einer der Zeigertypen deklariert werden sollte.

Da die Funktion eine Kopie des Arguments erhält, kann sie den Zeiger selbst nicht beeinflussen. Aber es kann schreiben, was immer es will, und dabei die Dereferenzierungsoperation * verwenden, um auf den Wert des ursprünglichen Arguments zuzugreifen.

Aufgabe. Schreiben Sie eine Funktion, um die Werte zweier Variablen auszutauschen, und rufen Sie sie über die Funktion aufhauptsächlich().

void swap(int *pa, int *pb) ( // Zeigerparameter

*pa = *pb; // füge b in a ein

*pb = temp; // füge a in b ein

void main(void) (

int i = 10, j = 20;

printf("i und j vor dem Austausch von Werten: %d %d\n", i, j);

swap(&i, &j); // Übergebe die Adressen der Variablen i und j

Die Funktion swap() kann die Werte zweier Variablen austauschen, auf die pa und pb zeigen, da die Adressen der Variablen an die Funktion übergeben werden, nicht ihre Werte. Innerhalb einer Funktion können Sie mithilfe von Standardzeigeroperationen auf den Inhalt von Variablen zugreifen und deren Werte austauschen.

Notiz! Jede Funktion, die Zeigerparameter verwendet, muss die Adressen der Argumente übergeben, wenn sie mit dem Adressoperator & aufgerufen wird.

Beim Aufruf einer Funktion mit Zeigerargumenten ist es nicht notwendig, die Adresse der Variablen als Parameter anzugeben. Sie können stattdessen den Wert eines Zeigers übergeben, der eine solche Adresse enthält.

void main(void) (

int i = 10, j = 20;

int *pi = &i, *pj =

printf("i und j vor dem Austausch von Werten: %d %d\n", i, j);

swap(pi, pj); // Übergebe die Adressen der Variablen i und j

printf("i und j nach Werteaustausch: %d %d\n", i, j);

Hier arbeiten wir mit Zeigern wie mit gewöhnlichen Variablen – wir senden ihnen mithilfe des Zuweisungsoperators Werte und geben sie dann an Funktionen weiter.

Abschluss: Wenn die aufgerufene Funktion zum Ändern von Variablen in der aufrufenden Funktion verwendet wird, sollten ihr als Parameter nicht die erforderlichen Variablen selbst, sondern entweder deren Adressen oder Zeiger darauf übergeben werden.

Aufgabe. Schreiben Sie zwei Funktionen, um die Summe zweier negativer Zahlen zu berechnen, und rufen Sie sie über die Funktion aufhauptsächlich(). Eingabedaten müssen in Funktionen eingegeben werdenhauptsächlich(). Die erste Funktion muss den angegebenen Wert zurückgeben. Stellen Sie in der zweiten Funktion die Kontrolle der Richtigkeit der Quelldaten sicher. Die Funktion muss zusätzlich zur Berechnung eines bestimmten Werts einen Hinweis auf die Richtigkeit der Quelldaten zurückgeben.

int sum1(int a, int b) (

int sum2(int a, int b, int *sum) (

wenn (a >= 0 || b >= 0)

0 zurückgeben; // Zeichen ungültiger Daten

Rückgabe 1; // Zeichen für korrekte Daten

void main(void) (

scanf(“%d %d”, &x, &y);

printf("Summe 1 = %d\n", sum1(x,y));

if (sum2(x,y,&s) == 1)

printf("Summe 2 = %d\n", s);

printf("Ungültige Daten!\n");

Ich entschuldige mich im Voraus für die prätentiöse Anmerkung zum „Platzieren von Punkten“, aber wir müssen Sie irgendwie in den Artikel locken.)) Ich für meinen Teil werde versuchen, sicherzustellen, dass die Zusammenfassung weiterhin Ihren Erwartungen entspricht.

Kurz worüber wir reden

Jeder weiß das bereits, aber zu Beginn möchte ich Sie daran erinnern, wie Methodenparameter in 1C übergeben werden können. Sie können „by reference“ oder „by value“ übergeben werden. Im ersten Fall übergeben wir der Methode denselben Wert wie zum Zeitpunkt des Aufrufs und im zweiten Fall eine Kopie davon.

Standardmäßig werden in 1C Argumente als Referenz übergeben und Änderungen an einem Parameter innerhalb einer Methode sind von außerhalb der Methode sichtbar. Hier kommt es für das weitere Verständnis der Frage darauf an, was genau Sie unter dem Wort „Parameteränderung“ verstehen. Das bedeutet also eine Neuzuweisung und nichts weiter. Darüber hinaus kann die Zuweisung implizit erfolgen, beispielsweise durch den Aufruf einer Plattformmethode, die etwas im Ausgabeparameter zurückgibt.

Wenn wir jedoch nicht möchten, dass unser Parameter als Referenz übergeben wird, können wir vor dem Parameter ein Schlüsselwort angeben Bedeutung

Prozedur ByValue(Wertparameter) Parameter = 2; EndProcedure-Parameter = 1; ByValue(Parameter); Bericht(Parameter); // gibt 1 aus

Alles funktioniert wie versprochen – das Ändern (oder vielmehr „Ersetzen“) des Parameterwerts ändert nicht den Wert außerhalb der Methode.

Nun, was ist der Witz?

Interessante Momente beginnen, wenn wir anfangen, nicht primitive Typen (Zeichenfolgen, Zahlen, Datumsangaben usw.), sondern Objekte als Parameter zu übergeben. Hier kommen Konzepte wie „flache“ und „tiefe“ Kopien eines Objekts sowie Zeiger ins Spiel (nicht in C++-Begriffen, sondern als abstrakte Handles).

Wenn wir ein Objekt (z. B. eine Wertetabelle) als Referenz übergeben, übergeben wir den Zeigerwert selbst (ein bestimmtes Handle), der das Objekt im Speicher der Plattform „hält“. Bei der Übergabe als Wert erstellt die Plattform eine Kopie dieses Zeigers.

Mit anderen Worten: Wenn wir in einer Methode bei der Übergabe eines Objekts als Referenz dem Parameter den Wert „Array“ zuweisen, erhalten wir zum Zeitpunkt des Aufrufs ein Array. Die Neuzuweisung des per Referenz übergebenen Werts ist vom Aufrufort aus sichtbar.

Prozedur ProcessValue(Parameter) Parameter = New Array; EndProcedure Table = New ValueTable; ProcessValue(Table); Report(ValueType(Table)); // gibt ein Array aus

Wenn wir das Objekt nach Wert übergeben, geht unsere Wertetabelle zum Zeitpunkt des Aufrufs nicht verloren.

Objektinhalt und -zustand

Bei der Wertübergabe wird nicht das gesamte Objekt kopiert, sondern nur dessen Zeiger. Die Objektinstanz bleibt gleich. Es spielt keine Rolle, wie Sie das Objekt übergeben, als Referenz oder als Wert – durch das Löschen der Wertetabelle wird auch die Tabelle selbst gelöscht. Diese Reinigung wird überall sichtbar sein, denn... Es gab nur ein Objekt und es spielte keine Rolle, wie genau es an die Methode übergeben wurde.

Prozedur ProcessValue(Parameter) Parameter.Clear(); EndProcedure Table = New ValueTable; Table.Add(); ProcessValue(Table); Report(Table.Quantity()); // gibt 0 aus

Bei der Übergabe von Objekten an Methoden arbeitet die Plattform mit Zeigern (bedingte Zeiger, keine direkten Analoga aus C++). Wenn ein Objekt als Referenz übergeben wird, kann die Speicherzelle der virtuellen 1C-Maschine, in der sich das Objekt befindet, durch ein anderes Objekt überschrieben werden. Wenn ein Objekt als Wert übergeben wird, wird der Zeiger kopiert und das Überschreiben des Objekts führt nicht dazu, dass der Speicherort mit dem Originalobjekt überschrieben wird.

Gleichzeitig jede Änderung Zustand Objekt (Reinigung, Hinzufügen von Eigenschaften usw.) verändert das Objekt selbst und hat überhaupt nichts damit zu tun, wie und wohin das Objekt übertragen wurde. Der Zustand einer Objektinstanz hat sich geändert; es kann eine Reihe von „Referenzen“ und „Werten“ darauf geben, aber die Instanz ist immer dieselbe. Durch die Übergabe eines Objekts an eine Methode erstellen wir keine Kopie des gesamten Objekts.

Und das ist immer wahr, außer...

Client-Server-Interaktion

Die Plattform setzt Serveraufrufe sehr transparent um. Wir rufen einfach eine Methode auf, und unter der Haube serialisiert die Plattform alle Parameter der Methode (wandelt sie in einen String um), übergibt sie an den Server und gibt die Ausgabeparameter dann an den Client zurück, wo sie deserialisiert werden und als leben wenn sie noch nie auf einem Server gewesen wären.

Wie Sie wissen, sind nicht alle Plattformobjekte serialisierbar. Hier wächst die Einschränkung: Nicht alle Objekte können vom Client an die Servermethode übergeben werden. Wenn Sie ein nicht serialisierbares Objekt übergeben, beginnt die Plattform, schlechte Wörter zu verwenden.

  • Eine explizite Erklärung der Absichten des Programmierers. Anhand der Methodensignatur können Sie deutlich erkennen, welche Parameter eingegeben und welche ausgegeben werden. Dieser Code ist einfacher zu lesen und zu warten
  • Damit eine Änderung des Parameters „by reference“ auf dem Server am Aufrufpunkt auf dem Client sichtbar ist, p Die Plattform selbst wird die an den Server übergebenen Parameter zwangsläufig per Link an den Client zurücksenden, um das am Anfang des Artikels beschriebene Verhalten sicherzustellen. Wenn der Parameter nicht zurückgegeben werden muss, kommt es zu einem Überlauf des Datenverkehrs. Um den Datenaustausch zu optimieren, sollten Parameter, deren Werte wir am Ausgang nicht benötigen, mit dem Wort Value gekennzeichnet werden.

Der zweite Punkt ist hier bemerkenswert. Um den Datenverkehr zu optimieren, gibt die Plattform den Parameterwert nicht an den Client zurück, wenn der Parameter mit dem Wort „Wert“ gekennzeichnet ist. Das ist alles großartig, führt aber zu einem interessanten Effekt.

Wie ich bereits sagte, erfolgt bei der Übertragung eines Objekts auf den Server eine Serialisierung, d. h. Es wird eine „tiefe“ Kopie des Objekts erstellt. Und wenn es ein Wort gibt Bedeutung Das Objekt wird nicht vom Server zurück zum Client übertragen. Wir addieren diese beiden Fakten und erhalten Folgendes:

&OnServerProcedureByLink(Parameter) Parameter.Clear(); EndProcedure &OnServerProcedureByValue(Value Parameter) Parameter.Clear(); EndProcedure &OnClient Procedure ByValueClient(Value Parameter) Parameter.Clear(); EndProcedure &OnClient Procedure CheckValue() List1= New ListValues; List1.Add("Hallo"); List2 = List1.Copy(); List3 = List1.Copy(); // Das Objekt wird vollständig kopiert, // an den Server übertragen und dann zurückgegeben. // das Löschen der Liste ist am Aufrufpunkt sichtbar ByRef(List1); // das Objekt wird komplett kopiert, // an den Server übertragen. Es kommt nicht zurück. // Das Löschen der Liste ist zum Zeitpunkt des Aufrufs von ByValue(List2) NICHT SICHTBAR. // nur der Objektzeiger wird kopiert // das Löschen der Liste ist zum Zeitpunkt des Aufrufs von ByValueClient(List3) sichtbar; Bericht(List1.Quantity()); Bericht(List2.Quantity()); Bericht(List3.Quantity()); Ende des Verfahrens

Zusammenfassung

Kurz gesagt lässt es sich wie folgt zusammenfassen:

  • Durch die Referenzübergabe können Sie ein Objekt mit einem völlig anderen Objekt „überschreiben“.
  • Durch die Wertübergabe können Sie das Objekt nicht „überschreiben“, Änderungen im internen Zustand des Objekts sind jedoch sichtbar, weil Wir arbeiten mit derselben Objektinstanz
  • Bei einem Serveraufruf wird mit VERSCHIEDENEN Instanzen des Objekts gearbeitet, weil Es wurde eine tiefe Kopie durchgeführt. Stichwort Bedeutung verhindert, dass die Serverinstanz zurück auf die Clientinstanz kopiert wird, und eine Änderung des internen Status eines Objekts auf dem Server führt nicht zu einer ähnlichen Änderung auf dem Client.

Ich hoffe, dass diese einfache Liste von Regeln es Ihnen erleichtert, Streitigkeiten mit Kollegen über die Übergabe von Parametern „nach Wert“ und „nach Referenz“ zu lösen.

Als ich mit dem Programmieren in C++ begann und mich intensiv mit Büchern und Artikeln beschäftigte, stieß ich immer auf den gleichen Rat: Wenn wir einer Funktion ein Objekt übergeben müssen, das sich in der Funktion nicht ändern soll, dann sollte es immer übergeben werden durch Bezugnahme auf eine Konstante(PPSK), außer in den Fällen, in denen wir entweder einen primitiven Typ oder eine Struktur ähnlicher Größe übergeben müssen. Weil In mehr als 10 Jahren Programmieren in C++ bin ich sehr oft auf diesen Rat gestoßen (und ich selbst habe ihn mehr als einmal gegeben), er ist schon lange in mich „aufgenommen“ – ich übergebe alle Argumente automatisch als Verweis auf eine Konstante . Doch die Zeit vergeht und es sind bereits 7 Jahre vergangen, seit wir C++11 mit seiner Move-Semantik zur Verfügung hatten, und in diesem Zusammenhang höre ich immer mehr Stimmen, die das gute alte Dogma in Frage stellen. Viele beginnen zu argumentieren, dass die Weitergabe unter Bezugnahme auf eine Konstante der Vergangenheit angehört und jetzt notwendig ist Wert übergeben(PPZ). Was sich hinter diesen Gesprächen verbirgt und welche Schlussfolgerungen wir daraus ziehen können, möchte ich in diesem Artikel diskutieren.

Buch Weisheit

Um zu verstehen, an welche Regel wir uns halten sollten, schlage ich vor, sich an Bücher zu wenden. Bücher sind eine hervorragende Informationsquelle, die wir nicht akzeptieren müssen, die aber durchaus hörenswert ist. Und wir beginnen mit der Geschichte, mit den Ursprüngen. Ich werde nicht herausfinden, wer der erste Apologet von PPSC war, ich werde lediglich das Buch als Beispiel nennen, das mich persönlich am meisten in Bezug auf die Verwendung von PPSC beeinflusst hat.

Meyers

Okay, hier haben wir eine Klasse, in der alle Parameter per Referenz übergeben werden. Gibt es Probleme mit dieser Klasse? Leider gibt es das, und dieses Problem liegt an der Oberfläche. Wir haben zwei funktionale Einheiten in unserer Klasse: Die erste nimmt einen Wert in der Phase der Objekterstellung an und die zweite ermöglicht es Ihnen, einen zuvor festgelegten Wert zu ändern. Wir haben zwei Einheiten, aber vier Funktionen. Stellen Sie sich nun vor, dass wir nicht zwei ähnliche Entitäten haben können, sondern 3, 5, 6, was dann? Dann werden wir mit einer starken Code-Aufblähung konfrontiert sein. Um nicht zu viele Funktionen zu erzeugen, wurde daher vorgeschlagen, auf Verknüpfungen in Parametern ganz zu verzichten:

Vorlage class Holder ( public: explizit Holder(T value): m_Value(move(value)) ( ) void setValue(T value) ( ​​​​m_Value = move(value); ) const T& value() const noexclusive ( return m_Value; ) privat: T m_Value);

Der erste Vorteil, der sofort ins Auge fällt, ist, dass deutlich weniger Code anfällt. Es ist sogar noch weniger davon als in der allerersten Version, da const und & entfernt wurden (obwohl move hinzugefügt wurde). Uns wurde jedoch immer beigebracht, dass die Weitergabe anhand von Referenzen produktiver ist als die Weitergabe anhand von Werten! So war es vor C++11 und so ist es immer noch, aber wenn wir uns jetzt diesen Code ansehen, werden wir sehen, dass hier nicht mehr kopiert wird als in der ersten Version. vorausgesetzt, dass T einen Verschiebungskonstruktor hat. Diese. PPSC selbst war und wird schneller sein als PPZ, aber der Code verwendet irgendwie die übergebene Referenz, und oft wird dieses Argument kopiert.

Dies ist jedoch nicht die ganze Geschichte. Im Gegensatz zur ersten Option, bei der wir nur kopieren, fügen wir hier auch Bewegung hinzu. Aber ein Umzug ist eine billige Operation, oder? Zu diesem Thema gibt es in dem von uns betrachteten Mayers-Buch auch ein Kapitel („Punkt 29“) mit der Überschrift: „Angenommen, dass Bewegungsoperationen nicht vorhanden, nicht billig und nicht genutzt sind.“ Die Hauptidee sollte aus dem Titel klar hervorgehen, aber wenn Sie Einzelheiten wissen möchten, lesen Sie ihn unbedingt durch – ich werde nicht weiter darauf eingehen.

Es wäre angebracht, hier eine vollständige vergleichende Analyse der ersten und letzten Methode durchzuführen, aber ich möchte nicht vom Buch abweichen, daher werden wir die Analyse auf andere Abschnitte verschieben und hier weiterhin Scotts Argumente berücksichtigen. Abgesehen von der Tatsache, dass die dritte Option offensichtlich kürzer ist als die zweite, worin sieht Scott den Vorteil von PPZ gegenüber PPSC im modernen Code?

Er sieht es darin, dass bei der Übergabe eines R-Wertes, d.h. Manche rufen so auf: Holder holder(string("me")); , die Option mit PPSC ermöglicht uns das Kopieren und die Option mit PPZ ermöglicht uns Bewegung. Wenn die Übertragung jedoch wie folgt abläuft: Holder holder(someLvalue); , dann verliert das PPZ definitiv, da es sowohl das Kopieren als auch das Verschieben durchführt, während es in der Version mit PPSC nur eine Kopie gibt. Diese. Es stellt sich heraus, dass die PPZ, wenn wir die reine Effizienz berücksichtigen, eine Art Kompromiss zwischen der Menge an Code und der „vollständigen“ (über && ) Unterstützung der Bewegungssemantik darstellt.

Deshalb hat Scott seinen Rat so sorgfältig formuliert und macht ihn so sorgfältig bekannt. Mir kam es sogar so vor, als hätte er es widerstrebend angesprochen, als stünde er unter Druck: Er konnte nicht anders, als Diskussionen zu diesem Thema in das Buch aufzunehmen, weil... Es wurde ziemlich ausführlich darüber diskutiert, und Scott war immer ein Sammler kollektiver Erfahrungen. Darüber hinaus führt er nur sehr wenige Argumente zur Verteidigung der PPZ an, dafür aber viele Argumente, die diese „Technik“ in Frage stellen. Wir werden uns seine Argumente dagegen in späteren Abschnitten ansehen, aber hier werden wir kurz das Argument wiederholen, das Scott zur Verteidigung der PPP vorbringt (im Geiste hinzufügen). „Wenn das Objekt die Bewegung unterstützt und es billig ist“): ermöglicht es Ihnen, das Kopieren zu vermeiden, wenn Sie einen R-Wert-Ausdruck als Funktionsargument übergeben. Aber genug der Qual von Meyers' Buch, lasst uns zu einem anderen Buch übergehen.

Übrigens, wenn jemand das Buch gelesen hat und überrascht ist, dass ich hier keine Option mit den von Mayers so genannten universellen Referenzen einfüge – die heute als Weiterleitungsreferenzen bekannt sind –, dann ist dies leicht zu erklären. Ich denke nur an PPZ und PPSC, weil... Ich halte es für eine schlechte Form, Vorlagenfunktionen für Methoden einzuführen, die keine Vorlagen sind, nur um die Übergabe beider Typen (rvalue/lvalue) als Referenz zu unterstützen. Ganz zu schweigen davon, dass der Code anders ausfällt (keine Konstanz mehr) und andere Probleme mit sich bringt.

Josattis und Unternehmen

Das letzte Buch, das wir uns ansehen werden, ist „C++ Templates“, das auch das aktuellste aller in diesem Artikel erwähnten Bücher ist. Es wurde Ende 2017 veröffentlicht (und 2018 ist im Buch angegeben). Im Gegensatz zu anderen Büchern befasst sich dieses ausschließlich mit Mustern und nicht mit Ratschlägen (wie Mayers) oder C++ im Allgemeinen wie Stroustrup. Daher werden hier die Vor- und Nachteile aus der Sicht des Schreibens von Vorlagen betrachtet.

Diesem Thema ist ein ganzes Kapitel 7 gewidmet, das den treffenden Titel „By value or by reference?“ trägt. In diesem Kapitel beschreiben die Autoren recht kurz, aber prägnant alle Übertragungsverfahren mit allen Vor- und Nachteilen. Eine Analyse der Wirksamkeit wird hier praktisch nicht gegeben und es wird davon ausgegangen, dass der PPSC schneller sein wird als der PPZ. Dennoch empfehlen die Autoren am Ende des Kapitels, das Standard-PPP für Vorlagenfunktionen zu verwenden. Warum? Denn bei Verwendung eines Links werden Template-Parameter vollständig angezeigt und ohne Link „verfallen“, was sich positiv auf die Verarbeitung von Arrays und String-Literalen auswirkt. Die Autoren glauben, dass, wenn sich herausstellt, dass ein PPP-Typ unwirksam ist, Sie immer std::ref und std::cref verwenden können. Dies ist ein Ratschlag. Um ehrlich zu sein, haben Sie viele Leute gesehen, die die oben genannten Funktionen nutzen möchten?

Was raten sie bezüglich PPSC? Sie empfehlen die Verwendung von PPSC, wenn die Leistung kritisch ist oder andere Probleme vorliegen gewichtig Gründe, PPP nicht zu verwenden. Natürlich sprechen wir hier nur von Standardcode, aber dieser Rat widerspricht direkt allem, was Programmierern seit einem Jahrzehnt beigebracht wird. Dies ist nicht nur ein Ratschlag, PPP als Alternative in Betracht zu ziehen – nein, es ist ein Ratschlag, PPSC zu einer Alternative zu machen.

Damit endet unsere Büchertour, denn... Ich kenne keine anderen Bücher, die wir zu diesem Thema konsultieren sollten. Kommen wir zu einem anderen Medienraum.

Netzwerkweisheit

Weil Wir leben im Zeitalter des Internets, da sollten Sie sich nicht allein auf Buchweisheiten verlassen. Darüber hinaus schreiben viele Autoren, die früher Bücher geschrieben haben, jetzt einfach Blogs und haben das Buchen aufgegeben. Einer dieser Autoren ist Herb Sutter, der im Mai 2013 auf seinem Blog „GotW #4 Solution: Class Mechanics“ einen Artikel veröffentlichte, der sich zwar nicht ausschließlich dem von uns behandelten Problem widmet, es aber dennoch berührt.

In der Originalversion des Artikels wiederholte Sutter also einfach die alte Weisheit: „Übergeben Sie Parameter als Referenz an eine Konstante“, aber wir werden diese Version des Artikels nicht mehr sehen, weil Der Artikel enthält den gegenteiligen Rat: „ Wenn Der Parameter wird trotzdem kopiert, dann übergeben Sie ihn als Wert.“ Wieder das berüchtigte „Wenn“. Warum hat Sutter den Artikel geändert und woher wusste ich davon? Aus den Kommentaren. Lesen Sie die Kommentare zu seinem Artikel; sie sind übrigens interessanter und nützlicher als der Artikel selbst. Es stimmt, dass Sutter nach dem Schreiben des Artikels schließlich seine Meinung geändert hat und solche Ratschläge nicht mehr gibt. Der Meinungswandel lässt sich in seiner Rede auf der CppCon 2014 nachlesen: „Back to the Basics! Grundlagen des modernen C++-Stils“. Schauen Sie unbedingt vorbei, wir kommen zum nächsten Internet-Link.

Und als nächstes haben wir die wichtigste Programmierressource des 21. Jahrhunderts: StackOverflow. Oder besser gesagt die Antwort: Zum Zeitpunkt der Erstellung dieses Artikels lag die Zahl der positiven Reaktionen bei über 1700. Die Frage ist: Was ist die Copy-and-Swap-Sprache? und, wie der Titel vermuten lässt, nicht ganz zu dem Thema, mit dem wir uns befassen. Doch mit seiner Antwort auf diese Frage berührt der Autor auch ein Thema, das uns interessiert. Er empfiehlt außerdem, PPZ zu verwenden, „wenn das Argument sowieso kopiert wird“ (es ist an der Zeit, auch dafür eine Abkürzung einzuführen, bei Gott). Und im Allgemeinen scheint dieser Rat im Rahmen seiner Antwort und des dort besprochenen Operators durchaus angemessen zu sein, aber der Autor nimmt sich die Freiheit, solche Ratschläge in einer umfassenderen Art und Weise zu geben, und nicht nur in diesem speziellen Fall. Darüber hinaus geht er über alle bisher besprochenen Tipps hinaus und fordert, dies auch im C++03-Code zu tun! Was hat den Autor zu solchen Schlussfolgerungen veranlasst?

Anscheinend hat sich der Autor der Antwort hauptsächlich von einem Artikel eines anderen Buchautors und Teilzeitentwicklers von Boost.MPL – Dave Abrahams – inspirieren lassen. Der Artikel trägt den Titel „Willst du Geschwindigkeit? Übergeben Sie den Wert.“ , und es wurde bereits im August 2009 veröffentlicht, d.h. 2 Jahre vor der Einführung von C++11 und der Einführung der Bewegungssemantik. Wie in früheren Fällen empfehle ich dem Leser, den Artikel selbst zu lesen, aber ich werde die Hauptargumente nennen (es gibt tatsächlich nur ein Argument), die Dave für die PPZ anführt: Sie müssen die PPZ verwenden , weil die „Skip Copy“-Optimierung damit gut funktioniert (Copy Elision), die in PPSC fehlt. Wenn Sie die Kommentare zum Artikel lesen, können Sie erkennen, dass die von ihm vertretenen Ratschläge nicht universell sind, was der Autor selbst bestätigt, wenn er auf die Kritik von Kommentatoren reagiert. Der Artikel enthält jedoch explizite Ratschläge (Richtlinien), das PPP zu verwenden, wenn das Argument trotzdem kopiert wird. Wer Interesse hat, kann sich übrigens den Artikel „Willst du Geschwindigkeit?“ durchlesen. Gehen Sie nicht (immer) am Wert vorbei.“ . Wie der Titel vermuten lässt, handelt es sich bei diesem Artikel um eine Antwort auf Daves Artikel. Wenn Sie also den ersten gelesen haben, lesen Sie unbedingt auch diesen!

Leider (zum Glück für einige) führen solche Artikel und (noch mehr) beliebte Antworten auf beliebten Websites zu einem massiven Einsatz zweifelhafter Techniken (ein triviales Beispiel), einfach weil dafür weniger geschrieben werden muss und das alte Dogma nicht mehr unerschütterlich ist – Sie können sich jederzeit auf „diesen beliebten Ratschlag“ berufen, wenn Sie an die Wand gedrängt werden. Jetzt schlage ich vor, dass Sie sich mit den verschiedenen Ressourcen vertraut machen, die uns Empfehlungen zum Schreiben von Code bieten.

Weil Da inzwischen auch diverse Standards und Empfehlungen online gestellt werden, habe ich mich entschieden, diesen Abschnitt als „Netzwerkweisheit“ einzustufen. Deshalb möchte ich hier über zwei Quellen sprechen, deren Zweck darin besteht, den Code von C++-Programmierern zu verbessern, indem sie ihnen Tipps (Richtlinien) zum Schreiben dieses Codes geben.

Die ersten Regeln, die ich berücksichtigen möchte, waren der letzte Tropfen, der mich dazu zwang, diesen Artikel aufzugreifen. Dieses Set ist Teil des Dienstprogramms clang-tidy und existiert nicht außerhalb davon. Wie alles, was mit Clang zu tun hat, ist dieses Dienstprogramm sehr beliebt und wurde bereits in CLion und Resharper C++ integriert (so bin ich darauf gestoßen). clang-tydy enthält also die Regel modernize-pass-by-value, die bei Konstruktoren funktioniert, die Argumente über PPSK akzeptieren. Diese Regel legt nahe, dass wir PPSC durch PPZ ersetzen. Darüber hinaus enthielt die Beschreibung dieser Regel zum Zeitpunkt des Verfassens des Artikels den Hinweis, dass es sich um diese Regel handelt Tschüss Funktioniert nur für Konstrukteure, aber sie (wer sind sie?) nehmen gerne Hilfe von denen an, die diese Regel auf andere Entitäten ausweiten. Dort gibt es in der Beschreibung auch einen Link zu Daves Artikel – es ist klar, woher die Beine kommen.

Um diesen Überblick über die Weisheit und maßgeblichen Meinungen anderer Leute abzuschließen, schlage ich schließlich vor, dass Sie einen Blick auf die offiziellen Richtlinien zum Schreiben von C++-Code werfen: C++ Core Guidelines, deren Hauptherausgeber Herb Sutter und Bjarne Stroustrup sind (nicht schlecht, oder?). Diese Empfehlungen enthalten also die folgende Regel: „Übergeben Sie für „in“-Parameter billig kopierte Typen nach Wert und andere nach Verweis auf const“, was die alte Weisheit vollständig wiederholt: PPSK überall und PPP für kleine Objekte. Dieser Tipp beschreibt mehrere Alternativen, die Sie in Betracht ziehen sollten. für den Fall, dass die Argumentübergabe optimiert werden muss. Aber PPZ ist nicht in der Liste der Alternativen enthalten!

Da ich keine anderen Quellen habe, die Aufmerksamkeit verdienen, schlage ich vor, mit einer direkten Analyse beider Übertragungsmethoden fortzufahren.

Analyse

Der gesamte vorangehende Text ist auf eine für mich etwas ungewöhnliche Weise geschrieben: Ich präsentiere die Meinung anderer und versuche sogar, meine eigene nicht auszudrücken (ich weiß, dass es schlecht ausgeht). Vor allem aufgrund der Tatsache, dass die Meinungen anderer, und mein Ziel war es, einen kurzen Überblick darüber zu geben, habe ich eine detaillierte Betrachtung bestimmter Argumente, die ich bei anderen Autoren gefunden habe, verschoben. In diesem Abschnitt werde ich mich nicht auf Autoritäten beziehen und keine Meinungen äußern; hier werden wir einige objektive Vor- und Nachteile von PPSC und PPZ betrachten, die mit meiner subjektiven Wahrnehmung gewürzt werden. Natürlich wird einiges von dem, was zuvor besprochen wurde, wiederholt, aber leider ist dies die Struktur dieses Artikels.

Hat PPP einen Vorteil?

Bevor ich also die Argumente dafür und dagegen betrachte, schlage ich vor, zu untersuchen, welchen Vorteil uns die Wertübergabe in welchen Fällen verschafft. Nehmen wir an, wir haben eine Klasse wie diese:

Klasse CopyMover ( public: void setByValuer(Accounter byValuer) ( m_ByValuer = std::move(byValuer); ) void setByRefer(const Accounter& byRefer) ( m_ByRefer = byRefer; ) void setByValuerAndNotMover(Accounter byValuerAndNotMover) ( m_ByValuerAndNotMover = byValuerAndNotMover; void. setR Gutachter (Accounter&& rvaluer) ( m_Rvaluer = std::move(rvaluer); ) );

Obwohl wir in diesem Artikel nur an den ersten beiden Funktionen interessiert sind, habe ich vier Optionen eingefügt, nur um sie als Kontrast zu verwenden.

Die Accounter-Klasse ist eine einfache Klasse, die zählt, wie oft sie kopiert/verschoben wurde. Und in der CopyMover-Klasse haben wir Funktionen implementiert, die es uns ermöglichen, die folgenden Optionen zu berücksichtigen:

    ziehen um bestandenes Argument.

    Wert übergeben, gefolgt von Kopieren bestandenes Argument.

Wenn wir nun jeder dieser Funktionen einen L-Wert übergeben, zum Beispiel so:

Accounter byRefer; Accounter byValuer; Accounter von ValuerAndNotMover; CopyMover copyMover; copyMover.setByRefer(byRefer); copyMover.setByValuer(byValuer); copyMover.setByValuerAndNotMover(byValuerAndNotMover);

dann erhalten wir folgende Ergebnisse:

Der offensichtliche Gewinner ist PPSK, weil... gibt nur eine Kopie, während PPZ eine Kopie und einen Zug gibt.

Versuchen wir nun, einen R-Wert zu übergeben:

CopyMover copyMover; copyMover.setByRefer(Accounter()); copyMover.setByValuer(Accounter()); copyMover.setByValuerAndNotMover(Accounter()); copyMover.setRvaluer(Accounter());

Wir erhalten Folgendes:

Hier gibt es keinen klaren Gewinner, denn... Sowohl PPZ als auch PPSK haben jeweils eine Operation, aber aufgrund der Tatsache, dass PPZ Bewegung und PPSK Kopieren verwendet, können wir PPZ den Sieg überlassen.

Aber unsere Experimente enden hier nicht; wir fügen die folgenden Funktionen hinzu, um einen indirekten Aufruf zu simulieren (mit anschließender Argumentübergabe):

Void setByValuer(Accounter byValuer, CopyMover& copyMover) ( copyMover.setByValuer(std::move(byValuer)); ) void setByRefer(const Accounter& byRefer, CopyMover& copyMover) ( copyMover.setByRefer(byRefer); ) ...

Wir werden sie genauso verwenden wie ohne sie, daher werde ich den Code nicht wiederholen (ggf. im Repository nachschauen). Für lvalue wären die Ergebnisse also so:

Beachten Sie, dass der PPSC den Abstand zum PPZ vergrößert und bei einer einzigen Kopie bleibt, während der PPZ bereits über bis zu 3 Operationen (eine weitere Bewegung) verfügt!

Jetzt übergeben wir den R-Wert und erhalten folgende Ergebnisse:

Jetzt hat die PPZ 2 Sätze und die PPSC hat immer noch ein Exemplar. Ist es nun möglich, PPZ als Gewinner zu nominieren? Nein, weil Wenn ein Zug zumindest nicht schlechter sein sollte als eine Kopie, können wir von zwei Zügen nicht dasselbe sagen. Daher wird es in diesem Beispiel keinen Gewinner geben.

Sie könnten mir widersprechen: „Autor, Sie haben eine voreingenommene Meinung und ziehen das ein, was für Sie von Vorteil ist.“ Sogar 2 Umzüge sind günstiger als das Kopieren!“ Ich kann dieser Aussage nicht zustimmen im Allgemeinen, Weil Wie viel schneller das Verschieben als das Kopieren ist, hängt von der jeweiligen Klasse ab, wir werden uns das „günstige“ Verschieben jedoch in einem separaten Abschnitt ansehen.

Hier haben wir eine interessante Sache angesprochen: Wir haben einen indirekten Aufruf hinzugefügt, und das PPP hat genau eine Operation in „Gewicht“ hinzugefügt. Ich denke, dass man kein Diplom der MSTU haben muss, um zu verstehen, dass bei Verwendung von PPZ umso mehr Operationen ausgeführt werden, je mehr indirekte Anrufe wir haben, während bei PPSC die Anzahl unverändert bleibt.

Es ist unwahrscheinlich, dass alles, was oben besprochen wurde, für irgendjemanden eine Offenbarung ist; wir haben möglicherweise nicht einmal Experimente durchgeführt – all diese Zahlen sollten für die meisten C++-Programmierer auf den ersten Blick offensichtlich sein. Allerdings bedarf ein Punkt noch einer Klärung: Warum hat die PZ im Fall von rvalue keine Kopie (oder einen anderen Zug), sondern nur einen Zug?

Nun, wir haben einen Blick auf den Übertragungsunterschied zwischen PPZ und PPSC geworfen, indem wir die Anzahl der Kopien und Bewegungen aus erster Hand beobachtet haben. Obwohl es offensichtlich ist, dass der Vorteil von PPZ gegenüber PPSC selbst in so einfachen Beispielen darin besteht, gelinde gesagt Nicht Offensichtlich komme ich immer noch etwas schief zu folgender Schlussfolgerung: Wenn wir das Funktionsargument trotzdem kopieren wollen, dann ist es sinnvoll, darüber nachzudenken, das Argument als Wert an die Funktion zu übergeben. Warum bin ich zu dieser Schlussfolgerung gekommen? Um reibungslos zum nächsten Abschnitt überzugehen.

Wenn wir kopieren...

Damit kommen wir zum sprichwörtlichen „Wenn“. Die meisten Argumente, auf die wir gestoßen sind, forderten nicht die universelle Umsetzung des PPP anstelle des PPSC, sondern nur, „wenn das Argument trotzdem kopiert wird“. Es ist Zeit herauszufinden, was an diesem Argument falsch ist.

Ich möchte mit einer kleinen Beschreibung beginnen, wie ich Code schreibe. In letzter Zeit ähnelt mein Codierungsprozess immer mehr TDD, d. h. Das Schreiben einer Klassenmethode beginnt mit dem Schreiben eines Tests, in dem diese Methode vorkommt. Wenn ich also mit dem Schreiben eines Tests beginne und nach dem Schreiben des Tests eine Methode erstelle, weiß ich immer noch nicht, ob ich das Argument kopieren werde. Natürlich werden nicht alle Funktionen auf diese Weise erstellt; oft weiß man bereits beim Schreiben eines Tests genau, welche Art von Implementierung es geben wird. Aber das passiert nicht immer!

Jemand könnte mir einwenden, dass es egal ist, wie die Methode ursprünglich geschrieben wurde. Wir können die Art und Weise ändern, wie wir das Argument übergeben, wenn die Methode Gestalt angenommen hat und uns völlig klar ist, was dort passiert (d. h. ob wir Kopier- oder Kopiervorgänge haben). nicht ). Ich stimme dem teilweise zu – Sie können es zwar auf diese Weise machen, aber das führt uns in ein seltsames Spiel, bei dem wir Schnittstellen ändern müssen, nur weil sich die Implementierung geändert hat. Das bringt uns zum nächsten Dilemma.

Es stellt sich heraus, dass wir die Schnittstelle basierend auf der Art und Weise, wie sie implementiert wird, modifizieren (oder sogar planen). Ich halte mich nicht für einen Experten für OOP und andere theoretische Berechnungen der Softwarearchitektur, aber solche Aktionen widersprechen eindeutig den Grundregeln, wenn die Implementierung die Schnittstelle nicht beeinträchtigen sollte. Natürlich dringen immer noch bestimmte Implementierungsdetails (sei es Funktionen der Sprache oder der Zielplattform) auf die eine oder andere Weise durch die Schnittstelle, aber Sie sollten versuchen, die Anzahl solcher Dinge zu reduzieren und nicht zu erhöhen.

Nun, Gott segne ihn, lasst uns diesen Weg gehen und dennoch die Schnittstellen ändern, je nachdem, was wir in der Implementierung tun, was das Kopieren des Arguments angeht. Nehmen wir an, wir haben diese Methode geschrieben:

Void setName(Name name) ( m_Name = move(name); )

und unsere Änderungen in das Repository übertragen. Mit der Zeit erhielt unser Softwareprodukt neue Funktionalitäten, neue Frameworks wurden integriert und es entstand die Aufgabe, die Außenwelt über Veränderungen in unserer Klasse zu informieren. Diese. Wir werden unserer Methode einen Benachrichtigungsmechanismus hinzufügen, der den Qt-Signalen ähneln soll:

Void setName(Name name) ( m_Name = move(name); emit nameChanged(m_Name); )

Gibt es ein Problem mit diesem Code? Essen. Für jeden Aufruf von setName senden wir ein Signal, sodass das Signal auch dann gesendet wird, wenn Bedeutung m_Name hat sich nicht geändert. Abgesehen von Leistungsproblemen kann diese Situation zu einer Endlosschleife führen, weil der Code, der die obige Benachrichtigung empfängt, irgendwie dazu kommt, setName aufzurufen. Um all diese Probleme zu vermeiden, sehen solche Methoden meist etwa so aus:

Void setName(Name name) ( if(name == m_Name) return; m_Name = move(name); emit nameChanged(m_Name); )

Wir haben die oben beschriebenen Probleme beseitigt, aber jetzt ist unsere Regel „Wenn wir trotzdem kopieren ...“ fehlgeschlagen – es gibt kein bedingungsloses Kopieren des Arguments mehr, jetzt kopieren wir es nur, wenn es sich ändert! Was sollen wir also jetzt tun? Schnittstelle ändern? Okay, ändern wir aufgrund dieses Fixes die Klassenschnittstelle. Was wäre, wenn unsere Klasse diese Methode von einer abstrakten Schnittstelle erben würde? Lasst es uns auch dort ändern! Gibt es viele Änderungen, weil sich die Implementierung geändert hat?

Sie könnten wieder Einwände gegen mich erheben, sagen sie, der Autor, warum versuchen Sie, bei Streichhölzern Geld zu sparen, wenn diese Bedingung da draußen funktioniert? Ja, die meisten Anrufe werden falsch sein! Gibt es da überhaupt Vertrauen? Wo? Und wenn ich mich entschieden habe, bei Streichhölzern zu sparen, war die Tatsache, dass wir PPZ verwendet haben, nicht eine Folge genau dieser Einsparungen? Ich führe lediglich die „Parteilinie“ fort, die Effizienz befürwortet.

Konstrukteure

Gehen wir kurz auf Konstruktoren ein, zumal es für sie in clang-tidy eine Sonderregel gibt, die für andere Methoden/Funktionen noch nicht funktioniert. Nehmen wir an, wir haben eine Klasse wie diese:

Klasse JustClass ( public: JustClass(const string& justString): m_JustString(justString) ( ) private: string m_JustString; );

Offensichtlich wird der Parameter kopiert und clang-tidy teilt uns mit, dass es eine gute Idee wäre, den Konstruktor wie folgt umzuschreiben:

JustClass(string justString): m_JustString(move(justString)) ( )

Und ehrlich gesagt fällt es mir schwer, hier zu argumentieren – schließlich kopieren wir ja eigentlich immer. Und wenn wir etwas durch einen Konstruktor übergeben, kopieren wir es meistens. Aber häufiger heißt nicht immer. Hier ist ein weiteres Beispiel:

Klasse TimeSpan ( public: TimeSpan(DateTime start, DateTime end) ( if(start > end) throw InvalidTimeSpan(); m_Start = move(start); m_End = move(end); ) private: DateTime m_Start; DateTime m_End; );

Hier kopieren wir nicht immer, sondern nur, wenn die Daten korrekt dargestellt sind. Natürlich wird dies in den allermeisten Fällen der Fall sein. Aber nicht immer.

Sie können ein weiteres Beispiel nennen, dieses Mal jedoch ohne Code. Stellen Sie sich vor, Sie haben eine Klasse, die ein großes Objekt akzeptiert. Die Klasse existiert schon seit langer Zeit und jetzt ist es an der Zeit, ihre Implementierung zu aktualisieren. Uns ist klar, dass wir nicht mehr als die Hälfte einer großen Anlage (die über die Jahre gewachsen ist) benötigen, vielleicht sogar weniger. Können wir etwas dagegen tun, indem wir einen Wert übergeben? Nein, wir können nichts machen, da trotzdem eine Kopie erstellt wird. Aber wenn wir PPSC verwenden würden, würden wir einfach ändern, was wir tun innen Designer. Und das ist der entscheidende Punkt: Mit PPSC steuern wir, was und wann bei der Implementierung unserer Funktion (Konstruktor) passiert, aber wenn wir PPZ verwenden, verlieren wir jegliche Kontrolle über das Kopieren.

Was können Sie aus diesem Abschnitt mitnehmen? Die Tatsache, dass das Argument „Wenn wir trotzdem kopieren ...“ ist sehr umstritten, weil Wir wissen nicht immer, was wir kopieren werden, und selbst wenn wir es wissen, sind wir oft nicht sicher, ob dies auch in Zukunft so bleiben wird.

Ein Umzug ist günstig

Von dem Moment an, als die Semantik der Bewegung auftauchte, begann sie einen ernsthaften Einfluss auf die Art und Weise zu haben, wie moderner C++-Code geschrieben wird, und mit der Zeit hat sich dieser Einfluss nur noch verstärkt: Kein Wunder, denn Bewegung ist so billig im Vergleich zum Kopieren. Aber ist es? Stimmt es, dass Bewegung ist? Stets günstige Operation? Das versuchen wir in diesem Abschnitt herauszufinden.

Binäres großes Objekt

Beginnen wir mit einem trivialen Beispiel. Nehmen wir an, wir haben die folgende Klasse:

Struktur-Blob ( std::array Daten; );

Normal Klecks(BDO, englisch BLOB), das in verschiedenen Situationen eingesetzt werden kann. Schauen wir uns an, was es uns kosten wird, als Referenz und als Wert anzugeben. Unser BDO wird etwa so verwendet:

Void Storage::setBlobByRef(const Blob& blob) ( m_Blob = blob; ) void Storage::setBlobByVal(Blob blob) ( m_Blob = move(blob); )

Und wir werden diese Funktionen so nennen:

Const Blob blob(); Lagerung; Lagerung; storage.setBlobByRef(blob); storage.setBlobByVal(blob);

Der Code für andere Beispiele wird mit diesem identisch sein, nur mit unterschiedlichen Namen und Typen, daher werde ich ihn für die übrigen Beispiele nicht angeben – alles befindet sich im Repository.

Bevor wir mit den Messungen fortfahren, versuchen wir, das Ergebnis vorherzusagen. Wir haben also ein 4 KB großes std::array, das wir in einem Storage-Klassenobjekt speichern möchten. Wie wir bereits herausgefunden haben, werden wir für PPSC eine Kopie haben, während wir für PPZ eine Kopie und einen Umzug haben werden. Aufgrund der Tatsache, dass es unmöglich ist, das Array zu verschieben, gibt es zwei Kopien für PPZ und eine für PPSC. Diese. Wir können von PPSC eine doppelte Leistungsüberlegenheit erwarten.

Werfen wir nun einen Blick auf die Testergebnisse:

Dieser und alle nachfolgenden Tests wurden auf demselben Computer mit MSVS 2017 (15.7.2) und dem /O2-Flag ausgeführt.

Die Praxis stimmte mit der Annahme überein, dass die Wertübergabe doppelt so teuer ist, da das Verschieben eines Arrays völlig gleichbedeutend mit dem Kopieren ist.

Linie

Schauen wir uns ein weiteres Beispiel an, einen regulären std::string . Was können wir erwarten? Wir wissen (ich habe dies im Artikel besprochen), dass moderne Implementierungen zwischen zwei Arten von Zeichenfolgen unterscheiden: kurz (ca. 16 Zeichen) und lang (die länger als kurz sind). Für kurze wird ein interner Puffer verwendet, der ein reguläres C-Array von char ist, aber lange werden bereits auf dem Heap platziert. Wir sind nicht an kurzen Schlangen interessiert, weil... Das Ergebnis wird dort das gleiche sein wie bei BDO, also konzentrieren wir uns auf lange Schlangen.

Wenn man also einen langen String hat, ist es offensichtlich, dass das Verschieben ziemlich kostengünstig sein sollte (bewegen Sie einfach den Zeiger), sodass Sie sich darauf verlassen können, dass das Verschieben des Strings überhaupt keine Auswirkungen auf die Ergebnisse haben sollte und die PPZ ein Ergebnis liefern sollte nicht schlechter als der PPSC. Lassen Sie es uns in der Praxis überprüfen und die folgenden Ergebnisse erzielen:

Wir werden mit der Erklärung dieses „Phänomens“ fortfahren. Was passiert also, wenn wir einen vorhandenen String in einen bereits vorhandenen String kopieren? Schauen wir uns ein triviales Beispiel an:

String first(64, „C“); string second(64, „N“); //... zweiter = erster;

Wir haben zwei 64-Zeichen-Strings, sodass beim Erstellen nicht genügend interner Puffer vorhanden ist, was dazu führt, dass beide Strings auf dem Heap zugewiesen werden. Jetzt kopieren wir den ersten in den zweiten. Weil Unsere Zeilengrößen sind gleich. Offensichtlich ist in „Second“ genügend Speicherplatz zugewiesen, um alle Daten von „First“ unterzubringen, also „second = first“; wird ein banales Memcpy sein, mehr nicht. Schauen wir uns aber ein leicht abgewandeltes Beispiel an:

String first(64, „C“); string second = first;

dann erfolgt kein Aufruf mehr von „operator=“, sondern der Kopierkonstruktor wird aufgerufen. Weil Da es sich um einen Konstruktor handelt, ist in diesem kein Speicher vorhanden. Es muss zunächst ausgewählt und erst dann kopiert werden. Diese. Dies ist die Speicherzuweisung und dann memcpy . Wie Sie und ich wissen, ist das Zuweisen von Speicher auf dem globalen Heap normalerweise ein teurer Vorgang, daher ist das Kopieren aus dem zweiten Beispiel teurer als das Kopieren aus dem ersten. Teurer pro Heap-Speicherzuweisung.

Was hat das mit unserem Thema zu tun? Das direkteste, denn das erste Beispiel zeigt genau, was mit PPSC passiert, und das zweite zeigt, was mit PPZ passiert: Für PPZ wird immer eine neue Zeile erstellt, während für PPSC die vorhandene Zeile wiederverwendet wird. Sie haben den Unterschied in der Ausführungszeit bereits gesehen, daher gibt es hier nichts hinzuzufügen.

Auch hier stehen wir vor der Tatsache, dass wir bei der Nutzung des PPP aus dem Kontext heraus arbeiten und daher nicht alle Vorteile nutzen können, die es bieten kann. Und wenn wir früher noch von theoretischen künftigen Veränderungen ausgingen, beobachten wir hier ein ganz konkretes Versagen der Produktivität.

Natürlich könnte mir jemand widersprechen, dass die Saiten auseinanderfallen und die meisten Typen nicht so funktionieren. Darauf kann ich Folgendes antworten: Alles, was zuvor beschrieben wurde, gilt für jeden Container, der sofort Speicher im Heap für ein Paket von Elementen zuweist. Wer weiß außerdem, welche anderen kontextsensitiven Optimierungen in anderen Typen verwendet werden?

Was sollten Sie aus diesem Abschnitt mitnehmen? Die Tatsache, dass das Verschieben wirklich günstig ist, bedeutet nicht, dass das Ersetzen des Kopierens durch Kopieren+Verschieben immer zu einem leistungsmäßig vergleichbaren Ergebnis führt.

Komplexer Typ

Schauen wir uns abschließend einen Typ an, der aus mehreren Objekten besteht. Dies sei die Klasse Person, die aus Daten besteht, die einer Person innewohnen. In der Regel handelt es sich hierbei um Ihren Vornamen, Nachnamen, Ihre Postleitzahl usw. Sie können dies alles als Zeichenfolgen darstellen und davon ausgehen, dass die Zeichenfolgen, die Sie in die Felder der Person-Klasse eingeben, wahrscheinlich kurz sind. Obwohl ich glaube, dass im wirklichen Leben das Messen kurzer Saiten am nützlichsten sein wird, werden wir uns dennoch Saiten unterschiedlicher Größe ansehen, um ein vollständigeres Bild zu erhalten.

Ich werde auch Person mit 10 Feldern verwenden, aber dafür werde ich keine 10 Felder direkt im Klassenkörper erstellen. Die Implementierung von Person verbirgt einen Container in seinen Tiefen – dies macht es bequemer, Testparameter zu ändern, praktisch ohne von der Funktionsweise abzuweichen, wenn Person eine echte Klasse wäre. Die Implementierung ist jedoch verfügbar und Sie können jederzeit den Code überprüfen und mir mitteilen, ob ich etwas falsch gemacht habe.

Also, los geht's: Person mit 10 Feldern vom Typ string , die wir mittels PPSC und PPZ in den Storage übertragen:

Wie Sie sehen, gibt es einen großen Leistungsunterschied, der die Leser nach den vorherigen Abschnitten nicht überraschen sollte. Ich glaube auch, dass die Person-Klasse „real“ genug ist, dass solche Ergebnisse nicht als abstrakt abgetan werden.

Übrigens habe ich bei der Vorbereitung dieses Artikels ein weiteres Beispiel vorbereitet: eine Klasse, die mehrere std::function-Objekte verwendet. Meiner Vorstellung nach sollte es auch einen Vorteil in der Leistung von PPSC gegenüber PPZ zeigen, aber es stellte sich genau das Gegenteil heraus! Aber ich nenne dieses Beispiel hier nicht, weil mir die Ergebnisse nicht gefielen, sondern weil ich keine Zeit hatte, herauszufinden, warum solche Ergebnisse erzielt wurden. Dennoch gibt es Code im Repository (Drucker), Tests – auch wenn jemand es herausfinden möchte, würde ich mich über die Ergebnisse der Forschung freuen. Ich habe vor, später auf dieses Beispiel zurückzukommen, und wenn vor mir niemand diese Ergebnisse veröffentlicht, werde ich sie in einem separaten Artikel betrachten.

Ergebnisse

Deshalb haben wir uns die verschiedenen Vor- und Nachteile der Wertübergabe und der Referenzübergabe an eine Konstante angesehen. Wir haben uns einige Beispiele angesehen und uns in diesen Beispielen die Leistung beider Methoden angesehen. Natürlich kann und erhebt dieser Artikel keinen Anspruch auf Vollständigkeit, aber meiner Meinung nach enthält er genügend Informationen, um eine unabhängige und fundierte Entscheidung darüber zu treffen, welche Methode am besten geeignet ist. Jemand könnte einwenden: „Warum eine Methode verwenden, fangen wir mit der Aufgabe an!“ Obwohl ich dieser These im Allgemeinen zustimme, bin ich in dieser Situation nicht damit einverstanden. Ich glaube, dass es nur eine Möglichkeit geben kann, Argumente in einer Sprache zu vermitteln: Das ist die Standardeinstellung.

Was bedeutet Standard? Das bedeutet, dass ich beim Schreiben einer Funktion nicht darüber nachdenke, wie ich das Argument übergeben soll, sondern einfach den „Standard“ verwende. Die Sprache C++ ist eine ziemlich komplexe Sprache, die viele Leute meiden. И по моему мнению, сложность вызвана не столько сложностью языковых конструкций, которые есть в языке (типичный программист может с ними никогда не столкнуться), сколько тем, что язык заставляет очень много думать: освободил ли я память, не дорого ли использовать здесь эту функцию usw.

Viele Programmierer (C, C++ und andere) sind misstrauisch und haben Angst vor C++, das nach 2011 auf den Markt kam. Ich habe viel Kritik gehört, dass die Sprache komplexer wird, nur noch „Gurus“ darin schreiben können usw. Persönlich glaube ich, dass dies nicht der Fall ist – im Gegenteil, das Komitee verwendet viel Zeit darauf, die Sprache anfängerfreundlicher zu gestalten, damit sich Programmierer weniger Gedanken über die Funktionen der Sprache machen müssen. Denn wenn wir uns nicht mit der Sprache herumschlagen müssen, haben wir Zeit, über die Aufgabe nachzudenken. Zu diesen Vereinfachungen gehören intelligente Zeiger, Lambda-Funktionen und vieles mehr, die in der Sprache enthalten sind. Gleichzeitig bestreite ich nicht, dass wir jetzt mehr lernen müssen, aber was ist falsch am Lernen? Oder gibt es in anderen populären Sprachen, die gelernt werden müssen, keine Veränderungen?

Außerdem habe ich keinen Zweifel daran, dass es Snobs geben wird, die antworten können: „Du willst nicht denken?“ Dann schreiben Sie in PHP.“ Ich möchte solchen Leuten nicht einmal antworten. Ich gebe nur ein Beispiel aus der Spielrealität: Wenn im ersten Teil von Starcraft ein neuer Arbeiter in einem Gebäude erstellt wurde, musste er manuell dorthin geschickt werden, damit er mit der Gewinnung von Mineralien (oder Gas) beginnen konnte. Darüber hinaus hatte jede Mineralienpackung eine Grenze, bei deren Erreichen die Erhöhung der Arbeitskräfte nutzlos war und sie sich sogar gegenseitig stören und die Produktion verschlechtern konnten. Dies wurde in Starcraft 2 geändert: Arbeiter beginnen automatisch mit dem Abbau von Mineralien (oder Gas), und es wird auch angezeigt, wie viele Arbeiter derzeit abbauen und wie hoch die Grenze dieser Lagerstätte ist. Dies vereinfachte die Interaktion des Spielers mit der Basis erheblich und ermöglichte ihm, sich auf wichtigere Aspekte des Spiels zu konzentrieren: den Aufbau einer Basis, das Sammeln von Truppen und die Vernichtung des Feindes. Es scheint, dass dies nur eine großartige Innovation ist, aber was begann im Internet! Die Leute (wer sind sie?) fingen an zu schreien, das Spiel sei „vermasselt“ und „sie haben Starcraft getötet.“ Offensichtlich konnten solche Botschaften nur von „Bewahrern geheimen Wissens“ und „Adepten hoher APM“ kommen, die gerne in einem „Elite“-Club waren.

Um auf unser Thema zurückzukommen: Je weniger ich darüber nachdenken muss, wie ich Code schreibe, desto mehr Zeit habe ich, über die Lösung des unmittelbaren Problems nachzudenken. Wenn ich darüber nachdenke, welche Methode ich verwenden soll – PPSC oder PPZ –, komme ich der Lösung des Problems kein bisschen näher, also weigere ich mich einfach, über solche Dinge nachzudenken und wähle eine Option: die Übergabe durch Referenz auf eine Konstante. Warum? Denn im Allgemeinen sehe ich keine Vorteile für PPP, Sonderfälle müssen gesondert betrachtet werden.

Es handelt sich um einen Sonderfall. Nachdem ich jedoch bemerkt habe, dass sich PPSC in gewisser Weise als Engpass herausstellt und wir durch die Umstellung der Übertragung auf PPZ eine erhebliche Leistungssteigerung erzielen werden, zögere ich nicht, das zu verwenden PPZ. Aber standardmäßig verwende ich PPSC sowohl in regulären Funktionen als auch in Konstruktoren. Und wenn möglich, werde ich diese spezielle Methode fördern, wo immer es möglich ist. Warum? Weil ich die Praxis der Förderung von PPP für bösartig halte, weil der Löwenanteil der Programmierer nicht sehr sachkundig ist (entweder im Prinzip oder einfach noch nicht in Schwung gekommen) und einfach Ratschläge befolgt. Wenn es außerdem mehrere widersprüchliche Ratschläge gibt, wählen sie den einfacheren aus, und das führt zu Pessimismus im Code, einfach weil jemand irgendwo etwas gehört hat. Oh ja, dieser Jemand kann auch einen Link zu Abrahams‘ Artikel bereitstellen, um zu beweisen, dass er Recht hat. Und dann sitzt man da, liest den Code und denkt: Liegt die Tatsache, dass der Parameter hier als Wert übergeben wird, daran, dass der Programmierer, der dies geschrieben hat, aus Java stammt, hat einfach viele „intelligente“ Artikel gelesen, oder besteht wirklich Bedarf an einem? technische Spezifikation?

PPSC ist viel einfacher zu lesen: Die Person kennt eindeutig die „gute Form“ von C++ und wir gehen weiter – der Blick bleibt nicht hängen. Die Verwendung von PPSC wird C++-Programmierern seit Jahren beigebracht. Was ist der Grund, sie aufzugeben? Dies führt mich zu einer weiteren Schlussfolgerung: Wenn eine Methodenschnittstelle ein PPP verwendet, sollte es auch einen Kommentar geben, warum dies so ist. In anderen Fällen muss der PPSC angewendet werden. Natürlich gibt es Ausnahmetypen, aber ich erwähne sie hier nicht, nur weil sie impliziert sind: string_view , initializer_list , verschiedene Iteratoren usw. Dies sind jedoch Ausnahmen, deren Liste sich je nach den im Projekt verwendeten Typen erweitern kann. Aber das Wesentliche bleibt seit C++98 dasselbe: Standardmäßig verwenden wir immer PPCS.

Für std::string wird es bei kleinen Strings höchstwahrscheinlich keinen Unterschied geben, wir werden später darüber sprechen.

Sei also Factorial(n) eine Funktion zur Berechnung der Fakultät einer Zahl n. Vorausgesetzt, wir „wissen“, dass die Fakultät 1 1 ist, können wir die folgende Kette konstruieren:

Fakultät(4)=Fakultät(3)*4

Fakultät(3)=Fakultät(2)*3

Fakultät(2)=Fakultät(1)*2

Aber wenn wir keine Endbedingung hätten, dass die Faktorfunktion bei n=1 1 zurückgeben sollte, dann wäre eine solche theoretische Kette nie zu Ende gegangen, und dies hätte ein Call-Stack-Overflow-Fehler sein können – Call-Stack-Overflow. Um zu verstehen, was ein Aufrufstapel ist und wie er überlaufen kann, schauen wir uns die rekursive Implementierung unserer Funktion an:

Funktion Fakultät(n: Ganzzahl): LongInt;

Wenn n=1, dann

Fakultät:=Fakultät(n-1)*n;

Ende;

Wie wir sehen können, ist es, damit die Kette korrekt funktioniert, vor jedem nächsten Funktionsaufruf an sich selbst notwendig, alle lokalen Variablen irgendwo zu speichern, damit beim Umkehren der Kette das Ergebnis korrekt ist (der berechnete Wert). der Fakultät von n-1 wird mit n multipliziert). In unserem Fall müssen bei jedem Aufruf der Fakultätsfunktion aus sich selbst alle Werte der Variablen n gespeichert werden. Der Bereich, in dem lokale Variablen einer Funktion beim rekursiven Aufruf gespeichert werden, wird Call Stack genannt. Natürlich ist dieser Stapel nicht unendlich und kann erschöpft sein, wenn rekursive Aufrufe falsch aufgebaut sind. Die Endlichkeit der Iterationen unseres Beispiels wird dadurch gewährleistet, dass bei n=1 der Funktionsaufruf stoppt.

Übergabe von Parametern nach Wert und Referenz

Bisher konnten wir den Wert im Unterprogramm nicht ändern tatsächlicher Parameter(d. h. der Parameter, der beim Aufruf des Unterprogramms angegeben wird), und in einigen Anwendungsaufgaben wäre dies praktisch. Erinnern wir uns an die Val-Prozedur, die den Wert von zwei ihrer tatsächlichen Parameter gleichzeitig ändert: Der erste ist der Parameter, in den der konvertierte Wert der String-Variablen geschrieben wird, und der zweite ist der Code-Parameter, in dem die Nummer des Fehlers steht Das Zeichen wird im Falle eines Fehlers während der Typkonvertierung platziert. Diese. Es gibt immer noch einen Mechanismus, mit dem ein Unterprogramm die tatsächlichen Parameter ändern kann. Dies ist dank verschiedener Möglichkeiten der Parameterübergabe möglich. Schauen wir uns diese Methoden genauer an.

Programmierung in Pascal

Übergabe von Parametern nach Wert

Im Wesentlichen haben wir auf diese Weise alle Parameter an unsere Routinen übergeben. Der Mechanismus ist wie folgt: Wenn ein tatsächlicher Parameter angegeben wird, wird sein Wert in den Speicherbereich kopiert, in dem sich das Unterprogramm befindet, und dieser Bereich wird dann gelöscht, nachdem die Funktion oder Prozedur ihre Arbeit abgeschlossen hat. Grob gesagt gibt es während der Ausführung einer Unterroutine zwei Kopien ihrer Parameter: eine im Bereich des aufrufenden Programms und die zweite im Bereich der Funktion.

Bei dieser Methode der Parameterübergabe dauert der Aufruf des Unterprogramms länger, da zusätzlich zum Aufruf selbst alle Werte aller tatsächlichen Parameter kopiert werden müssen. Wenn eine große Datenmenge an das Unterprogramm übergeben wird (z. B. ein Array mit einer großen Anzahl von Elementen), kann der Zeitaufwand für das Kopieren der Daten in den lokalen Bereich erheblich sein, was bei der Entwicklung von Programmen und Programmen berücksichtigt werden muss Engpässe in ihrer Leistung feststellen.

Bei dieser Übertragungsmethode können die tatsächlichen Parameter nicht vom Unterprogramm geändert werden, da sich die Änderungen nur auf einen isolierten lokalen Bereich auswirken, der nach Abschluss der Funktion oder Prozedur freigegeben wird.

Übergabe von Parametern als Referenz

Bei dieser Methode werden nicht die Werte der Aktualparameter in das Unterprogramm kopiert, sondern die Adressen im Speicher (Verknüpfungen zu Variablen), an denen sie liegen, übertragen. In diesem Fall ändert die Unterroutine bereits Werte, die nicht im lokalen Bereich liegen, sodass alle Änderungen für das aufrufende Programm sichtbar sind.

Um anzuzeigen, dass ein Argument als Referenz übergeben werden muss, wird das Schlüsselwort var vor seiner Deklaration hinzugefügt:

Prozedur getTwoRandom(var n1, n2:Integer; Bereich: Integer);

n1:=random(range);

n2:=random(range); Ende ;

var rand1, rand2: Ganzzahl;

Beginnen getTwoRandom(rand1,rand2,10); WriteLn(rand1); WriteLn(rand2);

Ende.

In diesem Beispiel werden Verweise auf zwei Variablen als tatsächliche Parameter an die Prozedur getTwoRandom übergeben: rand1 und rand2. Der dritte Aktualparameter (10) wird als Wert übergeben. Die Prozedur schreibt mit formalen Parametern