C++ Core Guidelines: Ein- und Ausgabe-Streams

Modernes C++  –  2 Kommentare

Wenn ein Programm mit seiner Umgebung interagiert, sind in C++ die Ein- und Ausgabe-Sstreams das Mittel der Wahl. Natürlich gibt es auch hier einige Regeln zu beachten. Welche, das zeigt der Artikel.

Die C++ Core Guidelines geben eine gute Einführung in iostreams, der ich nichts hinzufügen möchte: "iostreams is a type safe, extensible, formatted and unformatted I/O library for streaming I/O. It supports multiple (and user extensible) buffering strategies and multiple locales. It can be used for conventional I/O, reading and writing to memory (string streams), and user-defines extensions, such as streaming across networks (asio: not yet standardized)."

Überraschend war für mich vor allem die Tatsache, dass es nur fünf Regeln zu Iostreams in den C++ Core Guidelines gibt. Verschärfend kommt noch hinzu, dass die Regeln relativ wenig Inhalt besitzen. Das steht für mich im Widerspruch zu der Tatsache, dass iostreams in fast jedem Programm verwendet werden und damit sehr wichtig sind. Hier sind die fünf Kandidaten.

Um eine Geschichte aus den fünf Regeln zu machen, werde ich zusätzliche Information hinzufügen. Dies gilt aber nicht für die erste Regel.

SL.io.1: Use character-level input only when you have to

Zuerst einmal zeige ich das schlechte Beispiel aus den Guidelines. In diesem wird mehr als ein Zeichen eingelesen:

char c;
char buf[128];
int i = 0;
while (cin.get(c) && !isspace(c) && i < 128)
buf[i++] = c;
if (i == 128) {
// ... handle too long string ....
}

Ehrlich gesagt, ist dies eine schlechte Lösung für eine einfache Aufgabe. Dies gilt aber nicht für den nächsten Codeschnipsel:

string s;
s.reserve(128);
cin >> s;

Vermutlich ist der bessere Weg in diesem Fall auch der schnellere Weg.

Die nächste Regel formuliert eine Selbstverständlichkeit.

SL.io.2: When reading, always consider ill-formed input

Die Frage ist natürlich: Wie lässt sich mit falschen Daten umgehen? Jeder Stream hat einen Zustand assoziiert.

Zustand des Streams

Flags repräsentieren den Zustand des Streams. Die Methoden, um diese Flags zu manipulieren, benötigen die Header-Datei <iostream>.

Zur Anschaulichkeit stelle ich ein paar Beispiele vor, die für die verschiedenen Zustände der Streams verantwortlich sind.

Ursachen der Stream-Zustände

std::ios::eofbit

  • Jenseits des letzten gültigen Zeichens lesen.

std::ios::failbit

  • Falsch formatiertes Lesen.
  • Jenseits des letzten gültigen Zeichens lesen.
  • Das Öffnen einer Datei schlug fehl.

std::ios::badbit

  • Die Größe des Stream-Puffers kann nicht angepasst werden.
  • Die Codekonverierung der Stream-Puffers schlug fehl.
  • Ein Teil des Streams verursachte eine Ausnahme.

Lesen und Setzen des Stream-Zustands

stream.clear()

  • Initialisiert die Flags und setzt den Stream in den goodbit-Zustand

stream.clear(sta)

  • initialisiert die Flags und setzt den Stream in den sta-Zustand

stream.rdstate()

  • Gibt den Zustand des Streams zurück.

stream.setstate(fla)

  • Setzt das zusätzliche Flag fla.

Operationen auf Streams haben nur dann eine Auswirkung, wenn der Stream im goodbit-Zustand ist. Falls der Stream in dem badbit-Zustand ist, kann er nicht mehr in den goodbit-Zustand gesetzt werden.

// streamState.cpp

#include <ios>
#include <iostream>

int main(){

std::cout << std::boolalpha << std::endl;

std::cout << "In failbit-state: " << std::cin.fail() << std::endl;

std::cout << std::endl;

int myInt;
while (std::cin >> myInt){
std::cout << "Output: " << myInt << std::endl;
std::cout << "In failbit-state: " << std::cin.fail() << std::endl;
std::cout << std::endl;
}

std::cout << "In failbit-state: " << std::cin.fail() << std::endl;
std::cin.clear();
std::cout << "In failbit-state: " << std::cin.fail() << std::endl;

std::cout << std::endl;

}

Die Eingabe des Strings wrongInput bewirkt, dass std::cin in the std::ios::failbit-Zustand gesetzt wird. Konsequenterweise kann wrongInput und std::cin::fail() nicht dargestellt werden. Zuerst gilt es, den Zustand des std::cin-Stream in den goodbit-Zustand zu setzen.

Du kannst deine Ausgabe mit printf oder mit den Iostreams darstellen. Meine Empfehlung ist offensichtlich.

SL.io.3: Prefer iostreams for I/O

Das folgende Programm stellt zweimal die Daten auf die gleiche Art formatiert dar. Zuerst kommt printf mit einem Formatstring zum Einsatz und dann Iostreams mit einem Formatmanipulator:

// printfIostreams.cpp

#include <cstdio>

#include <iomanip>
#include <iostream>

int main(){

printf("\n");
printf("Characters: %c %c \n", 'a', 65);
printf("Decimals: %d %ld\n", 2011, 650000L);
printf("Preceding with blanks: %10d \n", 2011);
printf("Preceding with zeros: %010d \n", 2011);
printf("Doubles: %4.2f %E \n", 3.1416, 3.1416);
printf("%s \n", "From C to C++");

std::cout << std::endl;
std::cout << "Characters: " << 'a' << " " << static_cast<char>(65) << std::endl;
std::cout << "Decimals: " << 2011 << " " << 650000L << std::endl;
std::cout << "Preceding with blanks: " << std::setw(10) << 2011 << std::endl;
std::cout << "Preceding with zeros: " << std::setfill('0') << std::setw(10) << 20011 << std::endl;
std::cout << "Doubles: " << std::setprecision(3) << 3.1416 << " "
<< std::setprecision(6) << std::scientific << 3.1416 << std::endl;
std::cout << "From C to C++" << std::endl;

std::cout << std::endl;

}

Wie versprochen, dieselbe Ausgabe:

Aber warum sollten Iosteams printf vorgezogen werden? Es gibt einen entscheidenden Unterschied zwischen printf und Iostreams. Der bei printf angegebenen Format-String bestimmt den Datentyp und wie der Wert dargestellt wird; der Formatmanipulator des Iostreams bestimmt nur, wie der Wert dargestellt wird. Dieser Unterschied kann nicht deutlich genug hervorgehoben werden: Der Compiler bestimmt den richtigen Datentyp automatisch im Falle der Iostreams.

Was bedeutet es, wenn der Compiler automatisch den Datentyp bestimmt? Angenommen, du hattest einen arbeitsreichen Tag oder du bist noch ein relativ unerfahrener C++-Entwickler, sodass du den falschen Format-String verwendest. Das bedeutet, dein Programm besitzt undefiniertes Verhalten:

// printfIostreamsUndefinedBehaviour.cpp

#include <cstdio>

#include <iostream>

int main(){

printf("\n");

printf("2011: %d\n",2011);
printf("3.1416: %d\n",3.1416);
printf("\"2011\": %d\n","2011");
// printf("%s\n",2011); // segmentation fault

std::cout << std::endl;
std::cout << "2011: " << 2011 << std::endl;
std::cout << "3.146: " << 3.1416 << std::endl;
std::cout << "\"2011\": " << "2011" << std::endl;

std::cout << std::endl;

}

So schaut undefiniertes Verhalten auf meinem Rechner aus:

Der Compiler schreibt für gewöhnlich im Falle eines falschen Formatstring eine Warnung. Dafür gibt es aber keine Garantie. Zusätzlich ist die Frage, welche Relevanz eine Warnung besitzt, wenn die Deadline bereits weit überschritten ist. In diesem Fall ignorierst du die Warnung und wirst sie dir wohl später anschauen. Anstelle mit Fehlern umzugehen, sollten sie aber erst gar nicht gemacht werden.

Der Unterschied zwischen printf und Iostreams lässt mich unmittelbar an die wichtigste Designregel von Scott Meyers denken: "Make interfaces easy to use correctly and hard to use incorrectly."

Wie geht's weiter?

Für die Iostreams habe ich Formatmanipulatoren verwendet. Um dein Leben als Softwareentwickler einfach zu gestalten, solltest du eine paar Formatmanipulatoren kennen. Im nächsten Artikel zeige ich dir, welche.