Verwirrende Ausgabe



  • Kaum ist die eine beantwortet, habe ich schon wieder eine Frage. :p
    Ich habe eine Klasse Datum, man kann Instanzen von ihr beliebige Daten zuweisen oder auch das aktuelle Datum.
    Wie auch immer, ein bestimmtes Verhalten beim Vergleich zweier Daten verwirrt mich irgendwie. Folgender Ausschnitt aus der main()-Methode soll prüfen, ob zwei Daten gleich sind oder nicht:

    int main()
    {
        Datum datum1(01,01,0001); // Die Nullen zur Übersicht
        datum1.setJahr(2005);
        std::cout << datum1.asString() << std::endl; // Gibt 01.01.2005 aus
        Datum datum2(01,01,0001);
        datum2.setJahr(2004);
        std::cout << datum2.asString() << std::endl; // Gibt 01.01.2004 aus
        if (datum1.isEqual(datum2))
            std::cout << datum1.asString() << " ist dasselbe Datum wie " <<   datum2.asString() << std::endl;
        else  
            std::cout << datum1.asString() << " ist nicht dasselbe Datum wie " << datum2.asString() << std::endl; // Verwirrende Ausgabe
        std::cout << datum2.asString() << std::endl; // Gibt wieder 01.01.2004 aus
    }
    

    Das Problem ist, dass, vor und nach dem Vergleich, das richtige Jahr von datum2 (nämlich 2004) ausgegeben wird, allerdings wird in der if-else-Verzweigung zweimal 01.01.2005 ausgegeben, obwohl da die gleiche Methode asString() aufgerufen wird wie die anderen Male. Das verwirrt mich irgendwie.
    Die genaue Ausgabe lautet:
    "01.01.2005 ist nicht dasselbe Datum wie 01.01.2005".
    Die Vergleichsmethode:

    bool Datum::isEqual(const Datum& vergleich) const
    {
        if (jahr == vergleich.getJahr() && monat == vergleich.getMonat() && tag == vergleich.getTag())
            return true;
        else
            return false;
    }
    

    Die asString()-Methode:

    const std::string& Datum::asString() const
    {
        static std::string datum;
        std::stringstream format;
        format << std::setfill('0') << std::setw(2) << tag << '.' << std::setw(2) << monat << '.' << std::setw(4) << jahr;
        format >> datum;
        return datum;
    }
    

    Die getter und setter sind denke ich mal selbsterklärend.
    So, das ist jetzt ein bisschen viel, ich hoffe ihr könnt trotzdem helfen!

    Gruß



  • Was soll das

    static
    

    in Deiner asString-Methode? Das mach mal als erstes weg!
    Und dann gibst Du natürlich keine Referenz, sondern einen String zurück.



  • 😋 endlich mal Anfänger Code, den man anschauen kann. 👍

    gut nun zu dem Problem: lokale statische Variablen werden 1x definiert, nämlich beim ersten Aufruf der Funktion (in deinem Fall asString ). Jetzt wird asString 2x aufgerufen in der Zeile 12. Beide Male wird mit derselben Variablen datum gearbeitet. Wird die Funktion nun zum zweiten Mal aufgerufen, geht natürlich der Inhalt vom ersten Aufruf verloren, logisch, ist ja dieselbe Variable... und damit wird 2x dasselbe Datum ausgegeben.

    Du solltest das Ganze also so machen:

    const std::string Datum::asString() const
    	{
    		std::string datum;
    		std::stringstream format;
    		format << std::setfill('0') << std::setw(2) << tag << '.' << std::setw(2) << monat << '.' << std::setw(4) << jahr;
    		format >> datum;
    		return datum;
    	}
    


  • und nun kann man datum auch noch weglassen

    std::string Datum::asString() const // const Rückgabe macht hier keinen Sinn da eh kopiert wird
        {
            std::stringstream format;
            format << std::setfill('0') << std::setw(2) << tag << '.' << std::setw(2) << monat << '.' << std::setw(4) << jahr;
            return format.str();
        }
    


  • und isEqual kann man auch noch vereinfachen da der Vergleichsausdruck im if ja sowieso schon true oder false liefert.

    bool Datum::isEqual(const Datum& vergleich) const 
    { 
        return jahr == vergleich.getJahr() && monat == vergleich.getMonat() && tag == vergleich.getTag();
    }
    


  • Du solltest keine führenden Nullen "zur Übersicht" schreiben, denn das sind Oktalzahlen.



  • Belli schrieb:

    Was soll das

    static
    

    in Deiner asString-Methode? Das mach mal als erstes weg!
    Und dann gibst Du natürlich keine Referenz, sondern einen String zurück.

    Okay, static entfernt. Die Aufgabe war jedoch, dass die Methode eine Referenz auf einen String zurückliefert, deshalb musste/wollte ich das so lösen.

    out schrieb:

    😋 endlich mal Anfänger Code, den man anschauen kann. 👍

    danke 🙂

    gut nun zu dem Problem: lokale statische Variablen werden 1x definiert, nämlich beim ersten Aufruf der Funktion (in deinem Fall asString ). Jetzt wird asString 2x aufgerufen in der Zeile 12. Beide Male wird mit derselben Variablen datum gearbeitet. Wird die Funktion nun zum zweiten Mal aufgerufen, geht natürlich der Inhalt vom ersten Aufruf verloren, logisch, ist ja dieselbe Variable... und damit wird 2x dasselbe Datum ausgegeben.

    Du solltest das Ganze also so machen:

    const std::string Datum::asString() const
    	{
    		std::string datum;
    		std::stringstream format;
    		format << std::setfill('0') << std::setw(2) << tag << '.' << std::setw(2) << monat << '.' << std::setw(4) << jahr;
    		format >> datum;
    		return datum;
    	}
    

    Das heißt, außerhalb des if-else-Zweigs wird die statische Variable beliebig oft "neu" definiert für verschiedene Objekte, und innerhalb nur einmal? Das würde die Sache erklären. Würde es auch reichen, nur das static wegzumachen? Meine Aufgabe war es nämlich, wie gesagt, eine Referenz auf einen String zurückzugeben.
    Die anderen Tipps hab ich zur Kenntnis genommen, werd ich gleich mal verbessern 😉



  • Anonymus42 schrieb:

    Meine Aufgabe war es nämlich, wie gesagt, eine Referenz auf einen String zurückzugeben.

    ok, das ist dann etwas tricky. Wenn du das static wegmachst, dann hast du eine lokale Variable, die bei Verlassen des Scopes (in diesem Fall die Methode) zerstört wird. Du hast aber eine Referenz auf diese (inzwischen zerstörte) Variable rausgegeben...

    --> du musst die String-Repräsentation als Klassenmember halten. Entweder aktualiiserst du diese immer, wenn du irgendwas am Datum änderst und gibst in asString einfach nur die Referenz zurück, oder du aktualisiert die Variable erst in asString - dadurch hättest du weniger Overhead, aber teilweise auch einen inkonsistenten Zustand (Datum geändert, String-Member noch nicht).


  • Mod

    Anonymus42 schrieb:

    Das heißt, außerhalb des if-else-Zweigs wird die statische Variable beliebig oft "neu" definiert für verschiedene Objekte, und innerhalb nur einmal?

    Nein, das ist egal wo sie steht. eine static-Variable existiert nur einmal im gesamten Programmverlauf. Das heißt, du hast mit jedem Funktionsaufruf jeweils dein vorheriges Ergebnis überschrieben und wenn du dann im Nachhinein versuchst hast, es auszugeben, dann hast du bloß das allerneueste Ergebnis bekommen.

    Würde es auch reichen, nur das static wegzumachen?

    Jain. Das static muss weg, aber du darfst* keine Referenz auf ein lokales Objekt zurück geben. Da muss eine Kopie (die übrigens so gut wie sicher wegoptimiert wird) zurück gegeben werden.

    Meine Aufgabe war es nämlich, wie gesagt, eine Referenz auf einen String zurückzugeben.

    Das wäre eine ziemlich unsinnige Aufgabe. Bist du sicher dass du das richtig verstanden hast? Wahrscheinlich ist eher so etwas gemeint:

    void Datum::asString(string &result) const
    {
      // Hier wird result beschrieben
    }
    

    *: Genau genommen darfst du schon, es wird bloß nicht funktionieren.



  • Der Funktionsprototyp war so vorgegeben:

    const string& asString() const;
    

    Nein, das ist egal wo sie steht. eine static-Variable existiert nur einmal im gesamten Programmverlauf. Das heißt, du hast mit jedem Funktionsaufruf jeweils dein vorheriges Ergebnis überschrieben und wenn du dann im Nachhinein versuchst hast, es auszugeben, dann hast du bloß das allerneueste Ergebnis bekommen.

    Wieso aber klappt es dann im if-else-Zweig nicht, aber außerhalb schon? In der Verzweigung wird ja für datum2.asString() noch "01.01.2005" ausgegeben, während davor und danach "01.01.2004" ausgegeben wird.

    Das wäre eine ziemlich unsinnige Aufgabe. Bist du sicher dass du das richtig verstanden hast?

    Habe jetzt nachgeschaut, die Lösung des Autors sieht genauso aus wie meine, ich denke mal es ist ja auch ok, wenn die static-Variable dann einfach immer überschrieben wird. Bei den Lösungen ruft er die Funktion aber nicht in einem if-else-zweig auf, deshalb hab ich hier gefragt, warum das da nicht funktioniert 😉



  • Anonymus42 schrieb:

    Habe jetzt nachgeschaut, die Lösung des Autors sieht genauso aus wie meine, ich denke mal es ist ja auch ok, wenn die static-Variable dann einfach immer überschrieben wird. Bei den Lösungen ruft er die Funktion aber nicht in einem if-else-zweig auf, deshalb hab ich hier gefragt, warum das da nicht funktioniert 😉

    Wo kommt diese Aufgabe her? Das ist nämlich absoluter Schwachsinn. Vernünftig wäre in dieser Situation die Rückgabe des strings als Kopie, wier hier schon erwähnt wurde. Referenz ginge noch, wenn die Variable als Member gehalten wird. Aber das mit static zu lösen - da zweifle ich doch ernsthaft an der Kompetenz des Lösungsschreibers.



  • Anonymus42 schrieb:

    Habe jetzt nachgeschaut, die Lösung des Autors sieht genauso aus wie meine, ich denke mal es ist ja auch ok, wenn die static-Variable dann einfach immer überschrieben wird. Bei den Lösungen ruft er die Funktion aber nicht in einem if-else-zweig auf, deshalb hab ich hier gefragt, warum das da nicht funktioniert 😉

    Nein, es ist überhaupt nicht ok und birgt grosse Probleme im Bereich Multithreading. Auch sonst ist es unschön, da es eine (unsichtbare) Abhänigkeit zwischen den Instanzen der Klasse schafft.


  • Mod

    Bei jedem Aufruf von deiner Ausgabefunktion wird das vorherige Ergebnis überschrieben (man sagt: Deine Funktion hat Nebeneffekte). Weiterhin ist nicht festgelegt, in welcher Reihenfolge die Ausgabefunktionen in einem Ausdruck wie

    std::cout << datum1.asString() << " ist nicht dasselbe Datum wie " << datum2.asString() << std::endl;
    

    abgearbeitet werden, aber es ist garantiert, dass sie beide abgearbeitet werden, bevor die eigentliche Ausgabe geschieht. Das heißt, hier wird 2x das gleiche Datum ausgegeben (aber es ist unspezifiziert, welches von beiden). Nach dem if-Konstrukt wird wieder deine Ausgabefunktion aufgerufen, die wieder das alte Ergebnis überschreibt:

    std::cout << datum2.asString() << std::endl;
    

    Dies geschieht garantiert vor der Ausgabe, folglich wird korrekt datum2 ausgegeben.

    Merke: Funktionen mit Nebeneffekten in unspezifizierter Reihenfolge aufzurufen ist ganz, ganz böse! Daher ist die Konstruktion mit der Referenz auf statischen Speicher auch ziemlich unsinnig, wie ich schon erwähnte. Wie soll man diese Funktion jemals intuitiv benutzen können, wenn man auf solche Nebeneffekte achten muss? Ganz schlechter Lehrer 👎 .



  • Okay, dann wäre es nun geklärt. Ich erwähne jetzt besser mal nicht die Quelle, sonst gibt es hier nur noch Hateposts (es ist nicht Jürgen Wolf, soviel sei gesagt)... Mir gefiel die Lösung mit einem String als Rückgabewert sowieso von Anfang an besser.
    Danke nochmal an SeppJ und alle anderen!

    Gruß



  • Anonymus42 schrieb:

    Mir gefiel die Lösung mit einem String als Rückgabewert sowieso von Anfang an besser.

    const string& asString() const;
    

    Diese Signatur ist gar nicht mal so schlecht. Wenn mehrmals asString aufgerufen wird möchte man das Resultat irgendwie merken.

    mutable std::string as_string_cache;
    bool value_changed;
    
    const string& asString() const {
      if (value_changed)
        as_string_cache = ...
      return as_string_cache;
    }
    

    Lässt man Multithreading ausser Acht gibt es an diesem Code nichts auszusetzen. Das wäre eine gute Möglichkeit gewesen um das Schlüsselwort mutable einzuführen. Das mit static zu lösen ist hingegen Quatsch.



  • SeppJ schrieb:

    Weiterhin ist nicht festgelegt, in welcher Reihenfolge die Ausgabefunktionen in einem Ausdruck wie

    std::cout << datum1.asString() << " ist nicht dasselbe Datum wie " << datum2.asString() << std::endl;
    

    abgearbeitet werden, aber es ist garantiert, dass sie beide abgearbeitet werden, bevor die eigentliche Ausgabe geschieht.

    Das bezweifle ich.


  • Mod

    Bashar schrieb:

    SeppJ schrieb:

    Weiterhin ist nicht festgelegt, in welcher Reihenfolge die Ausgabefunktionen in einem Ausdruck wie

    std::cout << datum1.asString() << " ist nicht dasselbe Datum wie " << datum2.asString() << std::endl;
    

    abgearbeitet werden, aber es ist garantiert, dass sie beide abgearbeitet werden, bevor die eigentliche Ausgabe geschieht.

    Das bezweifle ich.

    Was genau?

    Die Undefiniertheit der Reihenfolge?
    Die Abarbeitung jeweils vor der Ausgabe?

    Ahh, ich glaube ich weiß was du meinst: Das beide abgearbeitet werden, bevor die Gesamtausgabe erfolgt? Nein, so hatte ich das auch nicht gemeint. Besser formuliert:
    ...aber es ist garantiert, dass sie jeweils beide abgearbeitet werden, bevor die eigentliche Ausgabe geschieht, wobei durchaus auch beide vorher abgearbeitet werden dürfen (aber nicht müssen).

    Ziemlich umständlich formuliert, aber dafür verwechselungssicher.



  • SeppJ schrieb:

    Bashar schrieb:

    SeppJ schrieb:

    Weiterhin ist nicht festgelegt, in welcher Reihenfolge die Ausgabefunktionen in einem Ausdruck wie

    std::cout << datum1.asString() << " ist nicht dasselbe Datum wie " << datum2.asString() << std::endl;
    

    abgearbeitet werden, aber es ist garantiert, dass sie beide abgearbeitet werden, bevor die eigentliche Ausgabe geschieht.

    Das bezweifle ich.

    [...]

    Ahh, ich glaube ich weiß was du meinst: Das beide abgearbeitet werden, bevor die Gesamtausgabe erfolgt? Nein, so hatte ich das auch nicht gemeint. Besser formuliert:
    ...aber es ist garantiert, dass sie jeweils beide abgearbeitet werden, bevor die eigentliche Ausgabe geschieht, [...]

    Na gut, aber in der Lesart ist es ja eine völlig triviale Aussage. Ohne diese Garantie hätte die Ausgabe ja nichts zum ausgeben.


  • Mod

    Bashar schrieb:

    Na gut, aber in der Lesart ist es ja eine völlig triviale Aussage. Ohne diese Garantie hätte die Ausgabe ja nichts zum ausgeben.

    Für uns. Dem TE war das anscheinend nicht wirklich klar, wie genau solche Ausdrücke abgearbeitet werden.



  • SeppJ schrieb:

    Bashar schrieb:

    Na gut, aber in der Lesart ist es ja eine völlig triviale Aussage. Ohne diese Garantie hätte die Ausgabe ja nichts zum ausgeben.

    Für uns. Dem TE war das anscheinend nicht wirklich klar, wie genau solche Ausdrücke abgearbeitet werden.

    War mir in der Tat nicht klar, dass da für zwei verschiedene Objekte die Funktion gleiches zurücklieferte. Habs aber dank der guten Erklärung jetzt verstanden (denk ich mal :p )


Log in to reply