Streams (4) - String Streams

Martin Kompf

String Streams sind ein universelles Mittel zur Konvertierung unterschiedlicher Datenformate ineinander. Vor allem lässt sich die oft benötigte Umwandlung von Strings in ihre numerischen Entsprechungen damit realisieren.

Die in den ersten drei Artikelfolgen vorgestellten Streams und ihre Manipulatoren haben wir bisher immer im Zusammenhang mit den Standard Ein- und Ausgabe-Streams std::cin, std::cout und std::cerr verwendet. Die iostream Library stellt aber noch weitere Streamtypen zur Verfügung: File Streams arbeiten mit Dateien zusammen, sie werden im Artikel Ein- und Ausgabe von Dateien vorgestellt. String Streams haben als Quelle oder Ziel ganz normale Variablen vom Typ std::string.

Achtung: Verwechseln Sie nicht die in diesem Artikel besprochenen String Streams aus <sstream> mit den veralteten char* Stream Klassen istrstream, ostrstream und strstream aus <strstream>! Diese bieten zwar im wesentlichen die gleiche Funktionalität an, bergen aber die latente Gefahr der Entstehung von Speicherlecks in sich. Leider haben noch nicht alle Hersteller von C++ Compilern die laut ANSI C++ Standard aktuelle <sstream> Implementierung bewerkstelligt. Abhilfe schafft hier die freie STLport C++ Library.

Verwandlungen

String Streams eignen sich hervorragend zur Konvertierung unterschiedlicher Datenformate ineinander. Insbesondere die Umwandlung numerischer Formate in Strings und umgekehrt ist eine vom Programmierer oft zu bewältigende Aufgabe. Aus der C Library sind hierfür die Funktionen sprintf und sscanf bekannt. Beide arbeiten mit kryptischen Formatierungsflags wie "%lf" oder "%04lx"; insbesondere sscanf neigt bei unsachgemäßer Anwendung zu Speicherüberschreibern und damit verbundenen Programmabstürzen. String Streams haben diesen Nachteil nicht, sie erlauben eine absolut typsichere Handhabung.

Eine Konvertierung von string in double analog sscanf erlaubt zum Beispiel das nachfolgende Codefragment:

#include <sstream>
#include <string>
using namespace std;

double d;
string s = "3345.5";

stringstream sstr;
sstr << s;
sstr >> d;

Jetzt enthält die Variable d die numerische Entsprechung von "3345.5". Das ganze funktioniert natürlich auch umgekehrt für die Umwandlung numerischer Daten in Strings:

int i = 200;

sstr << i;
sstr >> s;

Bei der Umwandlung von Strings in numerische Daten kann es zu Fehlern kommen, zum Beispiel kann die Zeichenkette "345,56" nicht in ein int konvertiert werden. Der Erfolg einer Konvertierung lässt sich durch Abfrage des End-of-File Flags des Streams testen: sstr.eof() muss true zurückliefern, wenn die Umwandlung erfolgreich war. Gab es hingegegen Fehler, dann stehen ja noch ungelesene Zeichen im Stream, so dass eof() dann false liefert.

Schließlich ist es noch möglich, auf den mit einem String Stream verbundenen String direkt mittels der Memberfunktion str() zuzugreifen. Dies kann zum Beispiel benutzt werden, um in einer Libraryfunktion Fehlertexte zusammenzubauen, die nicht direkt ausgegeben (das sollte man in einer Libraryfunktion tunlichst unterlassen!), sondern als String an das aufrufende Programm zurückgeliefert werden:

string getLastError()
{
    stringstream s;

    s << "Error " << errno << " in " << modname << ": " << strerror( errno);
    return s.str();
}

Universelle Schablone

Das bisher besprochene können wir nun in die Erstellung des universellen Funktionstemplates convert fließen lassen, welches die Konvertierung beliebiger Datentypen ineinander erlaubt. Als Resulat soll beispielsweise folgender Programmausschnitt möglich sein:

try {
    double d, e;
    string s, t;

    cout << "Enter value: " << flush;
    cin >> s;
    convert( s, d); // convert string to double
    e = d / 2.0;
    convert( e, t); // convert double to string
    cout << s + " / 2 = " + t << endl;
}
catch (const ConversionException& e) {
    cerr << "ERROR: " << e << endl;
}

Zunächst definieren wir die Exceptionklasse ConversionException, die immer dann geworfen werden soll, wenn eine Konvertierung nicht erfolgreich durchgeführt werden konnte. Die Exception soll ausserdem einen beschreibenden Fehlertext transportieren können und einen Ausgabeoperator << zur Ausgabe des Fehlertextes haben:

#include <sstream>
#include <string>
#include <exception>
#include <typeinfo>
using namespace std;

class ConversionException: public exception {
public:
    ConversionException( const string& mesg) : exception(), mesg_(mesg) {}
    string mesg_;
    friend ostream& operator<<( ostream& ost, const ConversionException& e)
    {return ost << e.mesg_;}
};

Nun kann das Funktionstemplate für die Konvertierungsfunktion convert aufgeschrieben werden. Anstelle konkreter Typen für die zu konvertieren Werte ival und oval werden Templateparameter verwendet:

template <class in_value, class out_value>
void convert( const in_value & ival, out_value & oval)
{
    stringstream ss;
    ss << ival; // insert value into stream
    ss >> oval; // get value from stream
    
    if (! ss.eof()) {
        // conversion error
        stringstream errs;
        errs << "conversion of "
            << ival << " to "
            << typeid(oval).name() << " failed";
        throw ConversionException( errs.str());
    }
}

String Streams werden hier an zwei Stellen benutzt: ss dient zur Konvertierung, errs hilft beim Zusammenbauen eines beschreibenden Fehlertextes für die Exception.

Selbstverständlich lassen sich auch die bisher vorgestellten (Standard- und nutzerdefinierten) Manipulatoren zusammen mit String Streams verwenden.