Werbung

  1. Diese Seite verwendet Cookies. Wenn du dich weiterhin auf dieser Seite aufhältst, akzeptierst du unseren Einsatz von Cookies. Weitere Informationen

[C++ Quicktip] Stream Formatierung 0.03

Stream-Flags setzen und sauber wieder zurücksetzen.

  1. -AB-
    Programmiersprache(n):
    C++
    Jeder kennt es: Man will schnell nur eine Zahl mit einer definierten Menge an Nachkommastellen ausgeben, und plötzlich wird *jede* weitere Zahl so formatiert ausgegeben - das Format ist keine Eigenschaft der Zahl oder eine temporäre Einstellung, die nur für diesen Aufruf gültig ist, sondern eine Eigenschaft des Streams selber.

    Wer also auf std::cout ein std::fixed schiebt, ändert damit von jetzt an für den Rest der Laufzeit das Verhalten von std::cout.

    Das ist unschön, oft will man das gar nicht, sondern stattdessen nur diesen einen Prozentwert anders formatiert ausgeben, und den Rest wieder normal... Und auch wenn man in C++11 endlich ein std::defaultfloat hat, kann man es nicht guten Gewissens auf den Stream schieben, denn der kann ja vorher schon verändert worden sein, beispielsweise weil alle Zahlen bisher in wissenschaftlicher Notation ausgegeben werden sollten.

    Einzige Möglichkeit ist, die format flags (und, wenn die Anzahl der Nachkommastellen auch geändert werden soll, ebenfalls die precision()) des Streams *vorher* zu sichern, die Formatänderungen vorzunehmen und dann den gesicherten Zustand wiederherzustellen.

    Das macht eine sonst so einfache Aufgabe natürlich kompliziert. Eine Möglichkeit ist es, eine Funktion zu schreiben, welche exakt diesen Handlungsablauf (state speichern, ändern, Daten ausgeben, state wiederherstellen) kapselt, wie in etwa...

    Code (C++):
    Quelltext kopieren
    1. void formatted_float(std::ostream& ostream, float value)
    2. {
    3.     //save
    4.     const std::streamsize defaultPrecision = ostream.precision();
    5.     const std::ios_base::fmtflags formerFlags = ostream.flags();
    6.  
    7.     //modify, print value
    8.     ostream << std::fixed << std::setprecision(2) << value;
    9.  
    10.     //restore
    11.     ostream.flags(formerFlags);
    12.     ostream.precision(defaultPrecision);
    13. }
    ...aber eine solche Funktion kann man nicht per << auf den Stream schieben.
    Wenn ich pro Zeile 3 Zahlen ausgeben möchte, von denen die dritte 2 Nachkommastellen haben soll, wird ein einfaches
    Code (C++):
    Quelltext kopieren
    1. std::cout << 3.14f << " ; " << 9.81f << " ; " << 42.1337f << std::endl;
    zu einem

    Code (C++):
    Quelltext kopieren
    1. std::cout << 3.141f << " ; " << 9.81f << " ; ";
    2. formatted_float(std::cout, 42.1337f);
    3. std::cout << std::endl;
    Ergibt 3 Zeilen statt einer.
    (Eine unschöne Möglichkeit wäre, formatted_float() einen std::string returnen zu lassen, damit könnte man den Aufruf mit in die <<-Kette schreiben. Es muss aber mindestens ein weiterer Stream und ein zusätzlicher std::string angelegt werden, deswegen: unschön.)

    Ein weiteres Problem mit dieser Methode ist die exception safety: schlägt der mittlere Teil (das Ausgeben des Wertes) aus irgendeinem Grund fehl, wird der vorige Zustand nicht mehr wieder hergestellt.

    Wir wissen bereits, dass wir mit dem RAII-Prinzip Code schreiben können, der auch im Exception-Fall noch den Zustand sauber hinterlässt. Zum Beispiel könnten wir eine SaveAndReset-Klasse schreiben, und sie in unsere Funktion einbauen:

    Code (C++):
    Quelltext kopieren
    1. struct SaveAndReset
    2. {
    3.     SaveAndReset(std::ostream& ostream) : mStream(ostream), mPrecision(ostream.precision()), mFlags(ostream.flags())
    4.     {}
    5.  
    6.     ~SaveAndReset()
    7.     {
    8.         mStream.precision(mPrecision);
    9.         mStream.flags(mFlags);
    10.     }
    11.  
    12.     std::ostream& mStream;
    13.     const std::size_t mPrecision;
    14.     const std::ios_base::fmtflags mFlags;
    15. };
    16.  
    17. void formatted_float(std::ostream& ostream, float value)
    18. {
    19.     SaveAndReset streamData(ostream);
    20.  
    21.     //modify, print value
    22.     ostream << std::fixed << std::setprecision(2) << value;
    23. }
    SaveAndReset lässt sich natürlich wiederverwenden und sogar verschachteln. Die exception safety haben wir erreicht, zurück zu unserem Problem, dass wir 3 Zeilen statt einer für die Ausgabe benötigen.

    Um bloß per <<-Operator einen Datentypen anders als vorgesehen auf einen Stream zu schreiben, muss der <<-Operator für einen eigenen Datentypen überladen werden. Das hieße für uns: Erst eine Wrapper-Klasse schreiben und dann den <<-Operator für diese festlegen.


    Wenn wir eine Prozentzahl also mit 2 Nachkommastellen und eventuell noch einem Prozentzeichen ausgeben wollen, schreiben wir flugs eine Klasse, die einen float wrappt - aber anders heißt, so dass wir einen überladenen stream<< Operator dafür schreiben können. Das ganze könnte so aussehen: (SaveAndReset von oben wird benutzt)

    Code (C++):
    Quelltext kopieren
    1. struct Percentage
    2. {
    3.     Percentage(float value) : mValue(value)
    4.     {}
    5.  
    6.     const float mValue;
    7. };
    8.  
    9. std::ostream& operator<<(std::ostream& ostream, const Percentage& percentage)
    10. {
    11.     SaveAndReset streamData(ostream);
    12.     ostream << std::fixed << std::setprecision(2) << percentage.mValue << "%";
    13.     return ostream;
    14. }
    15.  
    16. ....
    17.  
    18. std::cout << 3.141f << " ; " << 9.81f << " ; " << Percentage(42.51337f) << std::endl;
    Endlich passt die Ausgabe wieder in eine Zeile! Natürlich kann man noch weiter gehen und z.B. die Percentage-Klasse templatisieren, so dass sie nicht nur mit float funktioniert. Weiter kann man noch das schreiben auf den Stream vom Speichern und Wiederherstellen der flags trennen.

    Ich hab das testweise mal umgesetzt, wird aber zu lang für einen Quicktipp, kann man sicher noch schöner machen, ist eher als Denkanstoß gedacht, was man alles so schönes machen kann. :)

    PS: Ja, printf() ist eine tolle Funktion, kein Wunder, dass sie in Java nachträglich eingeführt wurde :)

    //edit:
    Oben aufgeführtes SaveAndReset funktioniert natürlich nur auf ostreams - dabei ist die Funktionalität für istreams dieselbe. (-> template) Außerdem sollte man noch die exception masks speichern und im Destruktor eventuell auftretende Exceptions unterdrücken. Hab ich beides für meinen privaten Gebrauch mal so gemacht.

Letzte Rezensionen

  1. German
    German
    5/5,
    Version: 2015-01-31
    Schade, dass es solche Lösungen noch nicht bis in den Standard geschafft haben. Der iomanip Header würde eigentlich eine gute Plattform bieten.