const String Referenz im Parameter - warum?



  • Hallo alle zusammen,

    beim lesen des C++ Buches von K&P sind mir ein paar Fragen gekommen.
    Es wird ein Konstruktor für ein Objekt definiert

    Konto::Konto (const string& k_name, long k_nr, double k_stand)
    {
    name = k_name
    ...
    ...
    }
    

    1. Warum benutzt man hier bei der Übergabe des Strings eine Referenz? Wo ist intern der Unterschied wenn ich & einfach weglassen würde?

    2. Welchen Vorteil hat es den string const zu machen? Die private Variable an die k_name zugewiesen wird, ist im Buch nicht als const deklariert.

    Über Antworten freue ich mich 🙂

    Lg



  • Scr00 schrieb:

    1. Warum benutzt man hier bei der Übergabe des Strings eine Referenz? Wo ist intern der Unterschied wenn ich & einfach weglassen würde?

    Weil man so eine Kopie spart.

    2. Welchen Vorteil hat es den string const zu machen? Die private Variable an die k_name zugewiesen wird, ist im Buch nicht als const deklariert.

    Das const ist nur da wegen der Referenz.
    Wäre die Referenz non-const, könnte man das Orginal bearbeiten (und keine Temporaries übergeben).
    Da man aber das Orginal nicht bearbeitet, sagt man das auch (const-correctness).



  • Hallo Nathan,

    erstmal vielen Dank für deine rasche Antwort.

    Nathan schrieb:

    Weil man so eine Kopie spart.

    Heißt das wenn ich einen String mit "" übergebe, dass dort bereits ein Objekt vom Typ String erzeugt wird, und wenn ich & weglasse, noch ein Objekt erzeugt wird und das erste in das zweite kopiert wird? Wenn ich & hingegen benutze, referenziert das zweite Objekt einfach das erste?



  • Scr00 schrieb:

    Nathan schrieb:

    Weil man so eine Kopie spart.

    Heißt das wenn ich einen String mit "" übergebe, dass dort bereits ein Objekt vom Typ String erzeugt wird, und wenn ich & weglasse, noch ein Objekt erzeugt wird und das erste in das zweite kopiert wird? Wenn ich & hingegen benutze, referenziert das zweite Objekt einfach das erste?

    Theoretisch schon.
    Aus "foo" wird ein string erzeugt und aus dieser String wird dann in den Parameter kopiert.
    Jeder Compiler wird da aber optimieren.

    Hast du aber bereits einen String, der in der main() existiert wird dieser String kopiert als Parameter und dann nochmal kopiert in die Variable. Auch hier ist aber Optimierungspotenzial besonders seit C++11 und RValue-Referenzen.



  • Scr00 schrieb:

    Heißt das wenn ich einen String mit "" übergebe, dass dort bereits ein Objekt vom Typ String erzeugt wird, und wenn ich & weglasse, noch ein Objekt erzeugt wird und das erste in das zweite kopiert wird?

    Sofern wir mal annehmen das der Compiler nichts optimiert wirst du ohne Referenzübergabe 3 Objekte erzeugen:
    1. Dein Initialstring (und ja, mit "" wird hier auch einer erzeugt).
    2. Der Parameter (Heißt daher auch Copy-By-Value)
    3. Die Membervariable, der du 2. zuweist.

    Bei einer Referenzübergabe entfällt das 2te Objekt, und nur der Ursprungs- und Zielwert existiert.

    Scr00 schrieb:

    Wenn ich & hingegen benutze, referenziert das zweite Objekt einfach das erste?

    Eine Referenz ist kein Objekt, sie referenziert nur ein selbiges. Eine Referenz muss noch nicht einmal irgendwo im Speicher existieren (könnte aber ebenso intern als Zeiger implementiert sein), da es nur ein Aliasname für das Ursprungselement ist.



  • Danke euch !


  • Mod

    Nathan schrieb:

    Theoretisch schon.
    Aus "foo" wird ein string erzeugt und aus dieser String wird dann in den Parameter kopiert.
    Jeder Compiler wird da aber optimieren.

    Ohne C++11 Moves wäre ich mir da nicht einmal so sicher. Über Funktionsgrenzen zu optimieren ist schon nicht ganz einfach. Das wird teilweise nur auf spezielle Aufforderung hin gemacht; schlägt aber auch fehl, wenn der Code der Funktion nicht verfügbar ist (Abhilfe Optimierung zur Linkzeit).

    Es ist schon eine gute Idee, die Kopie einzusparen, anstatt sich auf den Compiler zu verlassen. (Mit Move-Konstrukten ist dieser Tipp nicht mehr allgemeingültig.)



  • SeppJ schrieb:

    Nathan schrieb:

    Theoretisch schon.
    Aus "foo" wird ein string erzeugt und aus dieser String wird dann in den Parameter kopiert.
    Jeder Compiler wird da aber optimieren.

    Ohne C++11 Moves wäre ich mir da nicht einmal so sicher. Über Funktionsgrenzen zu optimieren ist schon nicht ganz einfach. Das wird teilweise nur auf spezielle Aufforderung hin gemacht; schlägt aber auch fehl, wenn der Code der Funktion nicht verfügbar ist (Abhilfe Optimierung zur Linkzeit).

    Ich dachte eher daran, dass der übergebene String nicht an den Parameter, sondern direkt an die Membervariable geht. Oder geht das nicht?



  • SeppJ schrieb:

    Über Funktionsgrenzen zu optimieren ist schon nicht ganz einfach.

    Bzgl copy elision bei Parametern und Rückgabewerten schon. Das ist nicht besonders kompliziert. Getrennte Übersetzung ist da auch gar kein Hindernis.


  • Mod

    Nathan schrieb:

    Ich dachte eher daran, dass der übergebene String nicht an den Parameter, sondern direkt an die Membervariable geht. Oder geht das nicht?

    Wäre ich mir auch nicht so sicher. Nicht einmal mit Initialisierungsliste. Probier es doch mal aus mit einer Klasse, die sich bei Kopie/Zuweisung/Move meldet. Dann packst du die Funktion in eine andere Übersetzungseinheit als den Aufruf (oder auch zum Test mal in die gleiche) und zählst mit. Gerne auch einmal mit Move und einmal ohne.



  • Nathan schrieb:

    Ich dachte eher daran, dass der übergebene String nicht an den Parameter, sondern direkt an die Membervariable geht. Oder geht das nicht?

    Einfache Antwort: Geht nicht.

    Längere Antwort: Das fällt nicht unter den Fällen, in denen der Standard explizit copy-elision erlaubt. Der Compiler dürfte das nur unter der "as-if"-Regel machen. Ich bin mir ziemlich sicher, dass das (inklusive eines Beweises, dass diese Optimierung nicht das Verhalten verändert, was beobachtet wird) kein Compiler hinbekommt.


  • Mod

    #include <string>
    struct Konto1
    {
        std::string name;
        Konto1(std::string name) : name(name) {}
    };
    
    struct Konto2
    {
        std::string name;
        Konto2(const std::string& name) : name(name) {}
    };
    
    struct Konto3
    {
        std::string name;
        Konto3(std::string name) : name(std::move(name)) {}
    };
    
    struct Konto4
    {
        std::string name;
        Konto4(const std::string& name) : name(name) {}
        Konto4(std::string&& name) : name(std::move(name)) {}
    };
    
    int main()
    {
        std::string foo;
        const std::string bar;
        Konto1 a1(foo);
        Konto1 b1(bar);
        Konto1 c1("");
        Konto2 a2(foo);
        Konto2 b2(bar);
        Konto2 c2("");
        Konto3 a3(foo);
        Konto3 b3(bar);
        Konto3 c3("");
        Konto4 a4(foo);
        Konto4 b4(bar);
        Konto4 c4("");
    }
    

    Hier mal die Übersicht, welche Konstruktoren wie oft aufgerufen werden.

    x/y/z  - Anzahl Aufrufe anderer/copy-/move-Konstruktoren
    					C++03				C++03 (copy-elision)			C++11					C++11 (copy-elision)
    			a		b		c			a		b		c			a		b		c			a		b		c
    Konto1		0/2/0	0/2/0	1/2/0		0/2/0	0/2/0	1/1/0		0/2/0	0/2/0	1/1/1		0/2/0	0/2/0	1/1/0
    Konto2		0/1/0	0/1/0	1/1/0		0/1/0	0/1/0	1/1/0		0/1/0	0/1/0	1/1/0		0/1/0	0/1/0	1/1/0
    Konto3		-----	-----	-----		-----	-----	-----		0/1/1	0/1/1	1/0/2		0/1/1	0/1/1	1/0/1
    Konto4		-----	-----	-----		-----	-----	-----		0/1/0	0/1/0	1/0/1		0/1/0	0/1/0	1/0/1
    

    Wie man sieht, ist die Variante per Referenz in keinem Fall schlechter als bei Übergabe per Value.



  • Nathan schrieb:

    Hast du aber bereits einen String, der in der main() existiert wird dieser String kopiert als Parameter und dann nochmal kopiert in die Variable. Auch hier ist aber Optimierungspotenzial besonders seit C++11 und RValue-Referenzen.

    Ja, aber kein sehr grosses.
    Alles was nen Namen hat ist keine RValue und wird daher per Default nie ein Move-Victim werden.
    Und Variablen in der main() haben normalerweise einen Namen.



  • camper schrieb:

    Hier mal die Übersicht, welche Konstruktoren wie oft aufgerufen werden.

    Schöne Tabelle 👍

    Bzgl. copy elision bei Funktionsargumenten habe ich gerade noch eine Frage: Wieso schafft der Compiler bei call-by-reference kein copy elision? Bei call-by-value schafft er es doch auch.

    #include <iostream>
    using namespace std;
    
    struct Konto
    {
    	Konto()                              { cout << "Standardkonstruktor" << '\n'; }
    	Konto( const Konto& ori )            { cout << "Kopierkonstruktor"   << '\n'; }
    	Konto& operator=( const Konto& ori ) { cout << "Zuweisungsoperator"  << '\n'; return *this; }
    	~Konto()                             { cout << "Destruktor"          << '\n'; }
    };
    
    void fkt_ref( const Konto& ori)
    {
    	Konto tmp(ori);
    }
    
    void fkt_value( Konto tmp )
    {
    }
    
    int main()
    {
    	cout << "fkt_ref:" << '\n';
    	fkt_ref( Konto() );
    
    	cout << '\n';
    
    	cout << "fkt_value:" << '\n';
    	fkt_value( Konto() );
    }
    

  • Mod

    Weil das Argument orin in fkt_ref kein temporäres Objekt ist. "Temporär" ist keine Eigenschaft eines Objektes als solches, sondern bezieht sich immer nur auf den Kontext, in dem das Objekt erstellt wurde. ich hab ein paar Jahre gebraucht, bis ich das wirklich verstanden hatte, dass die Begrifflichkeiten etwas verwirrend sind, kann ich also nachvollziehen.

    struct foo{};
    foo bar()
    {
        return std::move(foo()); // Move-Konstruktion kann nicht eliminiert werden, obwohl wir wissen, dass das Ergebnis von move auf ein temporäres Objekt verweist.
    }
    

    Definitionen im Standard haben nicht immer die Form

    An X is ...

    Zum Beispiel

    n3337 schrieb:

    3 Basic concepts [basic]
    /6
    A variable is introduced by the declaration of a reference other than a non-static data member or of an
    object. The variable’s name denotes the reference or object.

    Das ist auch eine Begriffsdefinition.

    Für temporäre Objekte haben wir

    n3337 schrieb:

    12.2 Temporary objects [class.temporary]

    Temporaries of class type are created in various contexts: binding a reference to a prvalue (8.5.3), returning
    a prvalue (6.6.3), a conversion that creates a prvalue (4.1, 5.2.9, 5.2.11, 5.4), throwing an exception (15.1),
    entering a handler (15.3), and in some initializations (8.5).

    Und auch hier ist das nicht einfach nur eine Beschreibung, wann temporäre Objekte entstehen, sondern was sie überhaupt sind.


Log in to reply