Idiomatische Lösung gesucht: in Plaintext-Dateiformat serialisieren



  • audacia schrieb:

    a) es gut sein kann, daß ich N verschiedene Formate und Serialisierungsmethoden für dieselben Datenstrukturen brauche, und ich b) viel von separation of concerns halte. Deshalb gibt es sowas wie Adapterklassen:

    a) Ob nun N freie Funktionen oder N Klassen mit ein paar virtuellen Methoden, who cares?
    b)Ja, das ist was gutes, d.h. aber nicht, dass der Usus von Java auch das beste fuer C++ ist. Adapterklassen als logische Konsequenz halte ich fuer falsch.

    mir an Vorteilen bringen soll, verstehe ich aber nicht.

    Es ist einfach, nah an C und erspart dir das Suchen in einem Stream.

    Was ist, wenn jemand mal ..

    Definiere Aunwendungsszenario, Anforderungen und moegliche Erweiterungen. Dann sehen wir weiter. Normalerweise wird niemand irgendwie sowas wollen. Und wenn, dann ist es nicht sinnvoll, alles durch ein Konzept erschlagen zu wollen.

    weil sich das Format so offensichtlich dafür eignet

    Widerspricht sich mit "N verschiedenen Formaten". Sorry, ich kann nicht in deinen Kopf sehen, ich weiss nicht was du dir denkst oder offensichtlich fuer dich ist.

    single-purpose-Klassen hinter Funktionen wie readFromTextFile()

    Nein, man braucht keine Klasse, die Funktionalitaet kann auch gleich direkt implementiert werden.



  • audacia schrieb:

    Vielleicht könnte man eine Art lightweight-Lexer/Tokenizer davorschalten; ähnlich wie die Stringstreams, aber nicht so restriktiv und eher zeichenmengenbasiert. Damit werde ich mal rumprobieren, aber es ist eher nichts C++-typisches, sowas sieht man eher in Pascal, wo es Typen wie "set of (Ansi)Char" gibt.

    Du könntest dir mal boost.Tokenizer anschauen, vielleicht ist das was für dich 🙂

    Ansonsten sehe ich aktuell nichts verbesserungswürdiges an deiner Umsetzung. 130 Zeilen C-Code als Parser sind überschaubar genug, da muss man keinen großen VOerkill betreiben um das "schön", "elegant" oder sonstwie aussehen zu lassen. Der Code sieht lesbar und verständlich aus, er macht ja offenbar was er soll - wenn das so bleibt kann der Code auch so bleiben 😉

    @knivil: "suchen im stream" scheint mir bei der simplen Syntax keine Anforderung zu sein. Bzgl. möglicher Erweiterungen stimme ich zu - über die sollte man sich erst den Kopf zerbrechen, wenn absehbar ist, dass sie kommen. Ebenso ack bzgl. packen von einer Funktion in eine Klasse. C++ ist eben nicht nur OOP, man kann auch wunderbar prozedural (oder auch in gewissem Sinne funktional) programmieren.



  • pumuckl schrieb:

    audacia schrieb:

    Vielleicht könnte man eine Art lightweight-Lexer/Tokenizer davorschalten; ähnlich wie die Stringstreams, aber nicht so restriktiv und eher zeichenmengenbasiert. Damit werde ich mal rumprobieren, aber es ist eher nichts C++-typisches, sowas sieht man eher in Pascal, wo es Typen wie "set of (Ansi)Char" gibt.

    Du könntest dir mal boost.Tokenizer anschauen, vielleicht ist das was für dich 🙂

    Auf den ersten Blick scheint es zu passen, auch wenn dieser charset-basierte Delphi-Tokenizer, den ich mir mal geschrieben hatte, irgendwie intuitiver war.
    Mal schauen, ob BCC mit Boost.Tokenizer klarkommt; die Header ziehen offenbar Teile von MPL hinein, was mich nicht so glücklich macht 😕

    pumuckl schrieb:

    Der Code sieht lesbar und verständlich aus, er macht ja offenbar was er soll - wenn das so bleibt kann der Code auch so bleiben 😉

    Das ist ein Wort 😉

    pumuckl schrieb:

    Ebenso ack bzgl. packen von einer Funktion in eine Klasse. C++ ist eben nicht nur OOP, man kann auch wunderbar prozedural (oder auch in gewissem Sinne funktional) programmieren.

    Im Allgemeinen ist dagegen nichts anzuwenden, aber in meinem Fall halte ich die Kritik für uninformiert und fehlgeleitet. Die Basisklasse (StorageReader/Writer) enthält die Serialisierungslogik, die Leaf-Klasse (TextFileStorageReader/Writer) implementiert das Lesen/Schreiben in einem konkreten Format mithilfe virtueller Funktionen. Wie und warum sollte ich das jetzt ohne Klasse implementieren?

    @knivil: ich glaube, es wäre produktiv, wenn du aufhören würdest, mir krampfhaft irgendwelche Fehler nachzuweisen. Seit deinem ersten Posting hier tust du im Grunde nichts anderes, als meine Frage schnell beiseitezuschieben (zu wenig Anforderungen; wie sieht die Datenstruktur aus; definiere ...) und dich fortan damit zu beschäftigen, mir zu zeigen, was ich angeblich alles falsch mache (virtuelle Funktionen im Header - ach nein, doch nicht; virtuelle Funktionen im Basisklassenkonstruktor - ach nein, doch nicht; Serialisierung gehört in die Klasse, die serialisiert wird - ach nein, doch nicht; aber wer Adapterklassen baut, macht Java, die Konsequenz ist in C++ falsch; du willst alles durch ein Konzept erschlagen, das macht man nicht; du brauchst diese Klasse nicht, kannst du auch ohne implementieren).



  • Nein, ich sage nur: ESerialisierungsframework fuer diesen Anwendungsfall ist kontraproduktiv. Das ist mein Vorschlag + Begruendung. Wenn es dir nicht passt, dann ignoriere ihn. Trotzdem habe ich den Eindruck, dass dich auf ein Konzept eingeschossen hast. Deswegen die Reibungspunkte.

    Beispiel, deine Storage-Klasse: Warum ein Array von serialisierbaren Objekten hier SerializableRange1D halten? Du kannst auch gleich ein Array von Range1D benutzen, beim Serialisieren in eine Datei wegen mir dein Adapterobjekt ad hoc konstruieren und abspeichern. D.h. der Zweck deiner Storage-Klasse ist mir nicht ersichtlich. Ich haette es anders gemacht, was deine urspruengliche Frage beantwortet.



  • knivil schrieb:

    Warum ein Array von serialisierbaren Objekten hier SerializableRange1D halten? Du kannst auch gleich ein Array von Range1D benutzen

    Weil eine SerializableEntity weitere Attribute hat. Etwa das "name"-Attribut habe ich in beiden Beispielen gezeigt.

    knivil schrieb:

    D.h. der Zweck deiner Storage-Klasse ist mir nicht ersichtlich.

    Wieder was anderes. Storage ist bloß ein Container, in den ich alles, was ich serialisieren will, reintun und mit Attributen (Name etc.) versehen kann, um es dann zu speichern, nach Namen zu filtern oder irgendwohin zu übergeben.

    Anyway, es ist sehr zuvorkommend, daß du dir Sorgen um meine Klassenhierarchie machst, aber der Punkt ist einfach, daß es nicht darum geht. Schon von Anfang an nicht.

    knivil schrieb:

    Wenn es dir nicht passt, dann ignoriere ihn.

    So soll es sein.



  • audacia schrieb:

    Was wäre eine möglichst C++-artige, aufwandsarme Lösung, um Dateien wie diese hier zu parsen und zu generieren?

    Hallo audica,

    Also die Lösung, die ich favorisiere und als C++-artig bezeichnen würde, besteht darin, zunächst Klassen oder Strukturen zu schaffen, wie man sie später im Programm benötigt. Wichtig ist dabei, das Serialisieren zunächst zu ignorieren - die Strukturen sollten zu dem Programm/Algorithmen passen, unabhängig von ihrer Ein- und Ausgabe.

    Z.B.:

    // --   [Range1D]
    struct Range1D
    {
        double min, max;
    };
    

    ich nehme an, das ist eine der Strukturen, so wie Du sie benötigst.

    Und erst im zweiten Schritt schaffe man sich dann einen Streamingoperator um diese struct Range1D z.B. einzulesen. Die Signatur ist damit quasi vorgegeben:

    std::istream& operator>>( std::istream& in, Range1D& rng1d );
    

    Eine Ausgabe würde äquivalent funktionieren:

    std::ostream& operator<<( std::ostream& out, const Range1D& rng1d );
    

    Anschließend kann man dann Instanzen dieser Struktur einlesen und ausgeben wie einen int oder double.

    Jetzt weiß ich natürlich zu wenig über das Format, das hier vorgegeben ist. Du schreibst Zwischending von INI und TSV. Also können die Block-Kennzeichnungen beliebig in der Datei stehen. Ich nehme mal der Einfachheit halber an, dass ab einem bestimmten Block (z.B. "[Range1D]") ein ganz bestimmtes Format kommt. In diesem Fall zwei Zeilen, die ohne Interesse sind und anschließend die beiden Werte 'min' und 'max' (bezeichnet mit 'From' und 'To').

    Also der Blockname definiert das , was dann kommt. Er gehört damit nicht selbst zu dem, was mit dem Streamingoperator von Range1D gelesen wird. Das muss ja vorher passieren. Wieder angenommen es existiert ein Manipulator skipline so ist die Implementierung der Lesefunktion kein Problem mehr:

    //      %Name=Section158.MarkedRange
    //      From    To
    //      4    18.9694
    std::istream& operator>>( std::istream& in, Range1D& rng1d )
    {
        return in >> skipline >> skipline >> rng1d.min >> rng1d.max;
    }
    

    zweimal eine Zeile überlesen dann die beiden Werte - fertig!

    skipline selber ist wie folgt implementiert:

    #include <istream>
    #include <limits> // numeric_limits
    //      skipline: überliest eine Zeile
    std::istream& skipline( std::istream& in )
    {
        return in.ignore( std::numeric_limits< std::streamsize >::max(), '\n' );
    }
    

    genauso geht man dann bei DiscreteScalarFunction1D alias ScalarFunction1D vor. Wobei mir hier nicht klar ist, wo die X- und Z-Werte abgelegt werden sollen.
    Ich würde Dir dazu eine kleine struct XZ (einen besseren Namen weiß ich nicht) vorschlagen. Und DiscreteScalarFunction1D sollte einen Container mit diesen Werten haben.

    Ohne mich jetzt in weiteren Erklärungen zu verlieren poste ich Dir mal einen Code, der die von Dir beschriebene Datei einliest. Wie Du das in Deine Serialisierungsobjekte einbaust, überlasse ich Dir.

    #include <iostream>
    #include <fstream>
    #include <vector>
    #include <string>
    #include <limits> // numeric_limits
    
    // --   Helferlein skipline, Char
    //      skipline: überliest eine Zeile
    std::istream& skipline( std::istream& in )
    {
        return in.ignore( std::numeric_limits< std::streamsize >::max(), '\n' );
    }
    
    //      Char<'X'> überliest das definierte Zeichen 'X'
    template< char C >
    std::istream& Char( std::istream& in )
    {
        char c;
        if( in >> c && c != C )
            in.setstate( std::ios_base::failbit );
        return in;
    }
    
    // --   [Range1D]
    struct Range1D
    {
        double min, max;
    }; 
    //      %Name=Section158.MarkedRange
    //      From    To
    //      4    18.9694
    std::istream& operator>>( std::istream& in, Range1D& rng1d )
    {
        return in >> skipline >> skipline >> rng1d.min >> rng1d.max;
    }
    
    // --   [ScalarFunction1D]
    struct XZ
    {
        double x_, z_;
    };
    std::istream& operator>>( std::istream& in, XZ& xz )
    {
        // Bem.: ggf. z_ von [nm] nach [um] konvertieren !?
        return in >> xz.x_ >> xz.z_;
    }
    
    struct DiscreteScalarFunction1D
    {
        Range1D argumentRange, valueRange;
        double resolution;
        std::string argumentUnit, valueUnit;
        std::vector<double> data;
        std::vector< XZ > xz_;
    };
    //      %Name=Section0
    //      x [µm]    z [nm]
    //      -4    -1564.73
    //      -3.21995    -1565.17
    //      ...
    std::istream& operator>>( std::istream& in, DiscreteScalarFunction1D& val )
    {
        val.xz_.clear();
        in >> skipline >> skipline;
        for( XZ xz; (in >> std::ws).good() && char(in.peek()) != '[' && in >> xz; )
            val.xz_.push_back( xz );
        //  Range1D argumentRange, valueRange;  // ??
        //  double resolution;
        //  std::string argumentUnit, valueUnit;
        //  std::vector<double> data;
        return in;
    }
    
    int main()
    {
        using namespace std;
        ifstream datei( "input.txt" );
        if( !datei.is_open() )
        {
            cerr << "Fehler beim Oeffnen der Datei" << endl;
            return -2;
        }
        for( string block; getline( datei >> Char<'['>, block, ']' ) >> skipline;  )
        {
            if( block == "Range1D" )
            {
                Range1D r;
                if( datei >> r )
                {
                    cout << "[Range1D] " << endl;
                    // usw. mache was mit 'r'
                }
            }
            else if( block == "ScalarFunction1D" )
            {
                DiscreteScalarFunction1D s;
                if( datei >> s )
                {
                    cout << "[ScalarFunction1D] " << " " << s.xz_.size() << "Eintraege" << endl;
                    // usw. mache was mit 's'
                }
            }
            else
            {
                cerr << "unbekannter Block: " << block <<  endl;
                datei.setstate( ios_base::failbit );
            }
        }
        if( datei.eof() )
        {
            cout << "alles gelesen" << endl;
        }
        return 0;
    }
    

    Wie Du siehst ersetzt hier allein das Mittelstück der Zeile 88 die ganze Methode tryReadHeader aus Deinem ersten Posting.

    Die Ausgabe ist hier

    [ScalarFunction1D]  9Eintraege
    [Range1D]
    [Range1D]
    alles gelesen
    

    Die einzige echte Schwierigkeit bestand darin, dass die Anzahl der XZ-Eelemte dynamisch sein soll (so nehme ich es an). Das heißt hier muss mit dem Lesen aufgehört werden, wenn ein neuer Block ('[') erscheint. Sauberer wäre vielleicht auf ein Zeichen abzufragen, was zu einer Zahl gehören kann - aber so ist es einfacher (siehe Zeile 68).
    Btw.: was soll mit den anderen Membern von DiscreteScalarFunction1D beim Einlesen passieren? Und was haben die Member DiscreteScalarFunction1D::argumentRange und DiscreteScalarFunction1D::valueRange mit dem [Range1D] in der Datei zu tun?

    Gruß
    Werner



  • Werner Salomon schrieb:

    Hallo audica,

    audacia 🙂

    Werner Salomon schrieb:

    Und erst im zweiten Schritt schaffe man sich dann einen Streamingoperator um diese struct Range1D z.B. einzulesen. Die Signatur ist damit quasi vorgegeben:

    std::istream& operator>>( std::istream& in, Range1D& rng1d );
    

    Eine Ausgabe würde äquivalent funktionieren:

    std::ostream& operator<<( std::ostream& out, const Range1D& rng1d );
    

    Okay, wäre möglich. Ich mag die C++-Streamoperatoren aber nicht sehr, und einer der Gründe ist, daß sie einen Datentyp auf eine bestimmte Art der Serialisierung festlegen. Wenn du einen Streamoperator für Dateiformat A schreibst, kannst du schlecht einen für Dateiformat B schreiben, ohne irgendwie ganz unschön mit using namespace zu zaubern.

    Werner Salomon schrieb:

    Jetzt weiß ich natürlich zu wenig über das Format, das hier vorgegeben ist. Du schreibst Zwischending von INI und TSV. Also können die Block-Kennzeichnungen beliebig in der Datei stehen. Ich nehme mal der Einfachheit halber an, dass ab einem bestimmten Block (z.B. "[Range1D]") ein ganz bestimmtes Format kommt. In diesem Fall zwei Zeilen, die ohne Interesse sind und anschließend die beiden Werte 'min' und 'max' (bezeichnet mit 'From' und 'To'). Also der Blockname definiert das , was dann kommt. Er gehört damit nicht selbst zu dem, was mit dem Streamingoperator von Range1D gelesen wird. Das muss ja vorher passieren.

    Die Zeilen dazwischen sind nicht ohne Interesse, das sind eben Attribute, sonst ist deine Lesart aber zutreffend.

    Werner Salomon schrieb:

    Wieder angenommen es existiert ein Manipulator skipline so ist die Implementierung der Lesefunktion kein Problem mehr:

    //      %Name=Section158.MarkedRange
    //      From    To
    //      4    18.9694
    std::istream& operator>>( std::istream& in, Range1D& rng1d )
    {
        return in >> skipline >> skipline >> rng1d.min >> rng1d.max;
    }
    

    zweimal eine Zeile überlesen dann die beiden Werte - fertig!

    skipline selber ist wie folgt implementiert:[...]

    Tatsächlich, sehr nützlich; von Manipulatoren hatte ich gelesen, den einen oder anderen auch schon benutzt, aber nie selbst welche geschrieben.

    Werner Salomon schrieb:

    Wie Du siehst ersetzt hier allein das Mittelstück der Zeile 88 die ganze Methode tryReadHeader aus Deinem ersten Posting.

    Zeile 88 ist sehr elegant und reizt mich, mir die ungeliebten C++-Streams doch nocheinmal genauer anzuschauen, aber mit tryReadHeader() hat sie nichts zu tun. tryReadHeader() parst Header mit oder ohne Einheiten von beliebiger Dimension, also etwa

    X [µm]	Y [nm]
    

    oder

    Col1	Col2[m]	Col3 <...> Col99 [m/s]
    

    oder

    From To
    

    . Das Pendant zu Zeile 88-113 sieht so aus (nur die Logik, die den Typnamen verarbeitet, ist woanders):

    virtual bool tryReadEntityName (std::string& name)
        {
            char buf[256];
            int result = file.scanf ("[%255[^]]]\n", buf);
            if (result == EOF)
                return false;
            else if (result != 1)
                throw std::runtime_error ("Cannot read line '" + file.readLine () + "': invalid type name format (expected '[<name>]'");
            else
            {
                name = buf;
                return true;
            }
        }
    

    Werner Salomon schrieb:

    Btw.: was soll mit den anderen Membern von DiscreteScalarFunction1D beim Einlesen passieren?

    Kommt mir vielleicht schon zu selbstverständlich vor. Die x-Werte sind bei DiscreteScalarFunction1D äquidistant und werden also nicht alle gebraucht. Stattdessen rechne ich sie in argumentRange und resolution um. Hier ist mein Deserialisierungscode:

    void Storage::NamedScalarFunction1D::readData (StorageReader* reader)
    {
        std::string labels[2], units[2];
        if (tryReadHeader (reader, labels, units, 2))
        {
            xLabel = labels[0];
            yLabel = labels[1];
            scalarFunction.argumentUnit = units[0];
            scalarFunction.valueUnit = units[1];
        }
    
        double data[2];
        scalarFunction.data.clear ();
        double min = std::numeric_limits<double>::max (),
               max = -std::numeric_limits<double>::max ();
        while (tryReadDataLine (reader, data, 2))
        {
            scalarFunction.data.push_back (data[1]);
            min = std::min (min, data[0]);
            max = std::max (max, data[0]);
        }
        scalarFunction.argumentRange = Range1Df (min, max);
        scalarFunction.resolution = scalarFunction.data.size () / scalarFunction.argumentRange.width (); // resolution ist aus irgendwelchen Gründen in [px/argumentUnit]
        scalarFunction.obtainValueRange (); // berechnet scalarFunction.valueRange anhand von scalarFunction.data
    }
    

    Beachte, daß readData() nichts von dem Header- oder Datenformat weiß. Ich könnte die Daten auch aus einem äquivalenten Binärformat beziehen, wenn tryReadHeader()/tryReadDataLine() vom jeweiligen StorageReader entsprechend implementiert wären, oder ich könnte auch leicht einen XML-Importer/-Exporter hinzufügen, falls irgendjemandes Richtlinie das erfordert. Natürlich ist die schiere Definition von tryReadHeader()/tryReadDataLine() auf dieses zeilenbasierte Format hin optimiert, aber immerhin wäre es nicht allzu umständlich möglich, für dieselben Serialisierungsmethoden ein anderes Format-Backend zu schreiben. Das ist hier nicht explizit erforderlich, aber ich finde es wichtig, daß sowas geht, und ich sehe nicht, wie man das mit Streams umsetzen würde.

    Vielen Dank für deine Mühe und den streambasierten Ansatz; das ist genau so etwas, was ich selber nie geschrieben hätte (da zu wenig vertraut mit und sehr argwöhnisch gegenüber den C++-Streams - bei sowas wie datei.setstate(ios_base::failbit); läufts mir immer kalt den Rücken runter), was aber dem üblichen oder zumindest vom Sprachstandard nahegelegten Idiom wohl nahe kommt.



  • audacia schrieb:

    Wenn du einen Streamoperator für Dateiformat A schreibst, kannst du schlecht einen für Dateiformat B schreiben, ohne irgendwie ganz unschön mit using namespace zu zaubern.

    dem kann ich jetzt nicht folgen. kannst das mal kurz erlaeutern ?

    Meep Meep



  • Meep Meep schrieb:

    audacia schrieb:

    Wenn du einen Streamoperator für Dateiformat A schreibst, kannst du schlecht einen für Dateiformat B schreiben, ohne irgendwie ganz unschön mit using namespace zu zaubern.

    dem kann ich jetzt nicht folgen. kannst das mal kurz erlaeutern ?

    Je nach Dateiformat will man denselben Typen anders serialisieren. Wenn ich XML ausgebe, will ich aus einer Range1D vielleicht sowas machen:

    <Range1D>
      <Name>Section42.MarkedRange</Name>
      <From>42.0</From>
      <To>47.7</To>
    </Range1D>
    

    In eine Binärdatei schreibe ich vielleicht den Namen als längenpräfizierten String und die min/max-Werte einfach als IEEE754-Double.
    In beiden Fällen muß das gleiche serialisiert werden: Name, min, max. Das weiß entweder Range1D selbst oder alternativ irgendeine Adapterklasse. Wie diese Information aber in der Datei landet, sollte Range1D und die Adapterklasse nicht interessieren, das kann man unabhängig davon allgemein behandeln.

    Mein Problem mit den Streamoperatoren ist, daß sie by design beides auf einmal machen, die Serialisierung (Name, min, max) und die Formatierung (XML, binär, ...), und daß du sie üblicherweise in den globalen Namespace packst (oder in den Namespace deines Typs, damit sie per ADL gefunden werden) und dein Streamoperator-Overload auf diese Weise zum Standard erhoben wird.



  • audacia schrieb:

    Werner Salomon schrieb:

    Hallo audica,

    audacia 🙂

    audacia, die Kühnheit; nein - wie konnte ich das überlesen - mea culpa 😉

    audacia schrieb:

    Ich mag die C++-Streamoperatoren aber nicht sehr, und einer der Gründe ist, daß sie einen Datentyp auf eine bestimmte Art der Serialisierung festlegen. Wenn du einen Streamoperator für Dateiformat A schreibst, kannst du schlecht einen für Dateiformat B schreiben, ...

    Das kann man sehr wohl. Man kann 'von außen' das Format auswählen. Das funktioniert über die 'ios_base storage functions'. Jedes std::ios_base-Objekt - und jeder Stream ist ein solches Objekt - besitzt einen erweiterbaren Speicher (storage), in dem man Integer- und Pointer-Werte ablegen und wieder abrufen kann. Das ganze funktioniert mit xalloc, iword und pword.

    Beispiele findest Du auch hier im Forum. Suche nach xalloc - z.B. hier.

    Gruß
    Werner



  • ah, ich glaub ich weiß jetz was du meinst.
    ich hab ein aehnliches problem ungefaehr so geloest:

    struct name
    {
       std::string vorname, nachname;
    }; /* struct name */
    
    class links
    {
       public: 
          links(const name &n) : m_n(n) { }
    
          void operator()(std::ostream &os) const
          {
             os << m_n.nachname.c_str() << ", " << m_n.vorname.c_str();
          }
    
       private:
          const name &m_n;
    };
    
    class rechts
    {
       public: 
          rechts(const name &n) : m_n(n) { }
    
          void operator()(std::ostream &os) const
          {
             os << m_n.vorname.c_str() << ", " << m_n.nachname.c_str();
          }
    
       private:
          const name &m_n;
    };
    
    std::ostream& operator<<(std::ostream &os, const rechts &n)
    {
       n(os);
       return os;
    }
    
    std::ostream& operator<<(std::ostream &os, const links &n)
    {
       n(os);
       return os;
    }
    
    int main()
    {
       name n;
       n.vorname = "Vorname";
       n.nachname = "Nachname";
    
       std::cout << links(n) << std::endl;
       std::cout << rechts(n) << std::endl;
    }
    
    Ausgabe:
    Nachname, Vorname
    Vorname, Nachname
    

    Meep Meep



  • Meep Meep schrieb:

    ah, ich glaub ich weiß jetz was du meinst.
    ich hab ein aehnliches problem ungefaehr so geloest:

    struct name
    {
       std::string vorname, nachname;
    }; /* struct name */
       // ... skip
    
       std::cout << links(n) << std::endl;
       std::cout << rechts(n) << std::endl;
    }
    

    Hallo Meep Meep,

    ich weiß nicht, aus was sich das was (s.o.) bezieht. Wenn Du die Antwort von audacia meinst, passt die Antwort nicht und wenn Du meinen letzten Beitrag meinst, dann ist es nicht das was ich meine.

    Dein Ansatz ist in Ordnung. Er hat aber den Nachteil, dass Du - in Deinem Fall 'links' und/oder 'rechts' jedes mal hinschreiben musst. Wenn Du aber eine größere hierarchische Struktur einlesen oder ausgeben willst, so würde das bedeuten, dass Du den Code zweimal schreibst; einmal mit 'links' und einmal mit 'rechts', um bei Deinem Beispiel zu bleiben.

    Besser ist, dass man z.B. nach dem Öffenen einer bestimmten Datei einfach schreiben kann:

    ifstream datei("...");
        ifstream >> format_b; // alles weitere im Format B einlesen
    

    Und dann ist es egal, wie komplex der einzulesende Code ist, der Stream ist dann mit B markiert und jede Einlesefunktion kann intern darauf Rücksicht nehmen.

    Gruß
    Werner



  • Hallo audacia,

    audacia schrieb:

    Mein Problem mit den Streamoperatoren ist, daß sie by design beides auf einmal machen, die Serialisierung (Name, min, max) und die Formatierung (XML, binär, ...), ...

    eher nicht - so hatte ich das nicht gemeint. Wenn Du Deine Serialisierungs-Klassenstruktur beibehalten willst, so bilden die Aufrufe der Streaming-Operatoren nicht die oberste Stufe. Sondern sie werden von den Funktionen aufgerufen, bei denen entschieden ist, dass jetzt z.B. aus einer Textdatei gelesen werden soll.
    Gerade das Lesen von Objekten (nicht das Parsen!) aus einer XML-Datei ist mit Streaming nicht zu machen. Eine XML-Datei von ihrer logischen Struktur ist eben kein Stream, sondern eher eine Baumstruktur oder ein assoziativer Container.

    Schaue Dir dazu mal boost.program_options bzw. boost.property_tree an. In beiden Fällen können Daten aus unterschiedlichen Quellen gelesen werden. Trotzdem bieten sie ein einheitliches User-Interface an - unabhängig von der Quelle.

    Gruß
    Werner



  • hallo Werner

    Werner Salomon schrieb:

    ich weiß nicht, aus was sich das was (s.o.) bezieht

    es bezieht sich auf folgendes:

    audacia schrieb:

    Je nach Dateiformat will man denselben Typen anders serialisieren. Wenn ich XML ausgebe, will ich aus einer Range1D vielleicht sowas machen:

    <Range1D>
    <Name>Section42.MarkedRange</Name>
    <From>42.0</From>
    <To>47.7</To>
    </Range1D>	
    Code:
    <Range1D>
      <Name>Section42.MarkedRange</Name>
      <From>42.0</From>
      <To>47.7</To>
    </Range1D>
    

    In eine Binärdatei schreibe ich vielleicht den Namen als längenpräfizierten String und die min/max-Werte einfach als IEEE754-Double.

    ich finde es nicht schlecht wenn man sowas dann so machen kann:

    class Range1D
    {
    ...
    };
    
    /* nun aehnlicher code wie fuer links und rechts */
    
    Range1D r1d;
    ...
    myfilestream << binary(r1d);
    // oder
    myfilestream << xml(r1d);
    

    hier kann man natuerlich auch viele andere wege gehen um zum selben ergebnis zu kommen. mir ist der stil aber einfach sympathischer vor allem wenn ich sowieso mit den streams arbeite. ich hab mir z.b. auch einige formatierungsobjekte geschrieben weil ich sie intuitiver finde:

    std::cout << base16("string") << oct(43) ...
    

    Meep Meep


Anmelden zum Antworten