🚀 go-pugleaf

RetroBBS NetNews Server

Inspired by RockSolid Light RIP Retro Guy

Article View: de.comp.lang.c++
Article #1842

Re: Probleme beim Vererben von fstream

#1842
From: Dietmar Kuehl
Date: Sat, 16 Mar 1996 09:34
347 lines
16911 bytes
Hi,

[Falls jemand meinen Artikel zu diesem Thread von Freitag abend gesehen
hat: Der war teilweise ein Flame. Daher habe ich ihn ge'cancel't: Bitte
ignoriert ihn einfach... Dieser hier wird dafuer ausfuehrlich und
technisch (allerdings nicht auf 100 Zeilen beschraenkt).]

Bernhard Ruch (Bernhard.Ruch@studi.epfl.ch) wrote:
: Dietmar Kuehl wrote:
: > 
: > Hi,

Nebenbei bemerkt: Bitte, quotet grundsaetzlich nur die releventen
Teile. Die Begruessung und die Signature gehoeren typischerweise NICHT
dazu. Neben verschwendeter Bandbreite (weniger die paar Bytes, als die
Zeit die zum Lesen der Quotes verwendet) gibt es auch ein
Copyright-Problem: Wenn der Text nicht signifikante Modifikationen
enthaelt, darf er ggf. garnicht nochmal gepostet werden! Etwa einen
fremden Artikel zu posten ist ein Verstoss gegen bestehende
Copyright-Gesetze! Zumindest in einigen Laendern und de.comp.lang.c++
wird auch ausserhalb von Deutschland gelesen...

: > Bernhard Ruch (Bernhard.Ruch@studi.epfl.ch) wrote:

[Beschreibung, wie ich denke dass '[io]stream' gedacht sind, geloescht.
Siehe ggf. vorherigen Artikel.]

: Ich muss vielleicht die Ausgangslage genauer beschreiben:

Das erscheint mir in der Tat noetig. Auch die unten beschriebene Lage
gibt noch reichlich Spielraum: Wenn ich bei allen Versuchen, eine
Interpretation davon zu geben danebenliege, dann kontaktiere mich bitte
per e-mail: Dann koennen wir das intern ausdiskutieren und ich poste
dann ggf. eine Beschreibung des Problems samt Loesung.

: Ich benoetige eine Klasse ObjectStream, welche die folgenden Operatoren enthaelt:
: (Da dies Spezifikationen meines Projektes sind, kann ich sie nicht aendern)

Schlechte Spezifikationen taugen nichts und nicht nur, dass man sie
aendern kann, man MUSS sie geradezu aenderen (selbstverstaendlich in
Absprache, da ja von den Spezifikationen vermutlich noch andere Sachen
abhaengen)! Eine Argumentation der Art "Ich muss mit einem schlechten
Design arbeiten, weil das die Spezifikationen so vorsehen" ist grob
straeflich. Es gibt nur eine Ausrede, ein [erkannt] schlechtes Design
zu verwenden: ALLE Alternativen sind noch schlechter. Schlechtes Design
ist ein beliebig grosser Kostenfaktor. Je groesser das Projekt ist,
desto groesser sind die Kosten, die man durch schlechtes Design
einfaehrt. Damit leben heisst, bewusst Kosten verursachen, anstatt zu
versuchen Kosten zu sparen. Ausserdem ist es nicht so, dass Du
demjenigen/derjenigen, der die Spezifaktion geschrieben hat schadest,
im Gegenteil: Der/die will den Programmteil von Dir ja verwenden. Wenn
der verbessert werden kann, ist allen geholfen!

: 	ObjectStream &operator << (char);
: 	ObjectStream &operator << (int);
: 	ObjectStream &operator << (float);
: 	etc.

: 	ObjectStream &operator >> (char &);
: 	ObjectStream &operator >> (int &);
: 	ObjectStream &operator >> (float &);
: 	etc.

: 	ObjectStream &operator << (const Object &);
: 	ObjectStream &operator >> (Object &);
: 	ObjectStream &operator << (const Object *);
: 	ObjectStream &operator >> (Object **);
: 	u.a.

: Dies antwortet auf die Frage, WAS geschrieben und gelesen werden soll.

Noe: Sollen die Sachen binaer oder formatiert geschrieben werden?
Sollen wirklich alle Operatoren Member sein oder ist nur die
entsprechende Aufruf-Syntax gefragt? Was ist "etc": Die built-in Typen
oder alle Typen fuer die ein Extractor/Inserter existiert? Soll
'ObjectStream' genau die genannten Sachen schreiben oder eigentlich
kompatibel zu '[io]stream' sein?

Es ist also, kurz gesagt, noch reichlich Freiraum fuer Interpretationen.

: Da es schon verschiedene Stream-Klassen gibt, welche die Operatoren <<
: und >> fuer die vordefinierten Typen (char, int, etc) enthalten, schien
: es mir das einfachste zu sein, diese Operatoren wiederzuverwenden, indem
: meine Klasse ObjectStream von einer dieser Stream-Klassen vererbt wird,
: und dann die neuen Operatoren hinzuzufuegen.	

OK, da hast Du ggf. recht: Wenn die Ein-/Ausgabe schoen formatiert sein
soll und so, dann will man die schon definierten Operatoren gerne
wiederverwenden. Allerdings kann man Objekte neuer Klassen formatiert
schreiben, ohne eine neue Klasse einzufuehren. Offenbar gibt es
allerdings irgendeinen [mir verborgenen] Grund, eine Klasse
'ObjectStream' zu definieren. Ich gehe also im folgenden erstmal davon
aus, dass 'ObjectStream' unausweichlich ist und auf jeden Fall
geschrieben werden muss. Weiter unten versuche ich dann nochmal dagegen
anzugehen, aber es scheint ja wichtige Gruende fuer 'ObjectStream' zu
geben. Eine moeglicher Grund koennte sein, dass 'ObjectStream' sowohl
Ein- als auch Ausgabe machen soll: Ein derartiger Stream ist im
Standard nicht vorgesehen.  Allerdings tut man sich bei den
Rueckgabewerten der verschiedenen Operatoren schwer, wenn man diese
wirklich von '[io]stream' erbt: Die liefern naemlich, komme was wolle,
entweder einen 'istream' oder einen 'ostream'. Wenn das nicht reicht,
dann braucht man andere Funktionen. Aber eins nach dem anderen: Erstmal
gucken, was 'ObjectStream' eigentlich sein soll.

: Nun zur Frage, WIE geschrieben und gelesen werden soll: Da diese Operatoren
: auf ein File angewendet werden sollen, draengte es sich auf, die Klasse 
: ObjectStream von fstream (anstatt iostream) zu vererben, da sie somit alle
: File-spezifischen Eigenschaften von fstream wiederverwenden kann.

Aha, 'ObjectStream' soll auf ein File schreiben bzw. von einem File
lesen. 'fstream' dafuer zu verwenden ist allerdings auf jeden Fall
gewagt (das habe ich schon in meinem vorherigen Artikel erwaehnt), da
'fstream' keine Standard-Klasse ist. Ausserdem ist 'fstream' ganz
sicher nicht dazu gedacht, dass von 'fstream' (public) abgelitten
wird.  Besser ist es direkt von 'istream' und 'ostream' abzuleiten und
einen 'filebuf' mitzugeben oder vielleicht garnicht abzuleiten. Was man
dann noch fuer Zugriffe fuer den 'filebuf' zur Vergueng stellt liegt im
Ermessen von demjenigen, der es verwenden soll: '[io]fstream' fuegen eh
nur ein paar (forwarding) Funktionen hinzu ('rdbuf()', 'isopen()',
'open()' und 'close()'), so dass es sich nicht wirklich lohnt dafuer
etwaige Problem in Kauf zu nehmen (etwa dass bei virtueller Vererbung
zwei 'filebuf's erzeugt werden oder so).

Da es es sich eh nicht lohnt, die Operatoren von 'istream' bzw.
'ostream' zu erben (die liefern wie gesagt immer den entsprechenden
Stream), versuche ich es garnicht erst! Das heisst allerdings nicht,
dass ich die entsprechenden Operatoren nicht verwenden werde. Nur mache
ich es halt anders. Also erstmal den Stream bauen (ich zeige nur die
"aeusseren" Details, schliesslich werde ich weder als Programmierer
noch als Consultant bezahlt... Naja, eigentlich schon, aber nicht von
den Leuten, denen ich hier helfe):

  // Fuer eine Implementation von 'auto_ptr' siehe etwa "More Effective
  // C++", S.Meyers, Addison-Wesley
  #include <auto_ptr>

  class ObjectStream
  {
  private:
    auto_ptr<filebuf> i_buf;  // darueber wird die IO gemacht
    istream           i_in;
    ostream           i_out;
  
  public:
    explicit ObjectStream(const char* fname, ios::openmode mode = in):
      i_buf(new filebuf()),
      i_in(i_buf.get()),
      i_out(i_buf.get())
    {
      i_buf->open(fname, mode);
    }
    // ... mal sehen, was noch so gebraucht wird.
  };

Also, was ein 'ObjectStream' macht, ist irgendwie aehnlich zu einem
'ios'-Objekt (vielleicht lohnt es sich von 'ios' abzuleiten, aber das
ist nicht klar...), nur einfacher: Es wird ein Pointer zu einem
'filebuf' verwaltet ('i_buf'). Die Verwaltung besteht insbesondere auch
darin, dass das Objekt bei allen passenden Gelegenheiten geloescht wird
(daher der 'auto_ptr'). Anstatt einem 'filebuf' kann man was beliebiges
anderes nehmen, sofern es nur eine 'concrete class' ist.

Die Member 'i_in' und 'i_out' werden benutzt, um die bereits
vorhandenen Inserter/Extractor zu verwenden. Dabei gibt es drei
moegliche Ansaetze (in der Erlaeuterung ist 'os' ein 'ObjectStream' und
't' ist ein Objekt von einem Typ, fuer den es einen Inserter und einen
Extractor gibt):

  1. Es genuegt, dass man die Expressions 'os << t' und 'os >> t'
     schreiben kann und der Typ der Expression ist jeweils
     'ObjectStream&'.
  2. Alle Operatoren muessen wirklich Memberfunktionen sein (d.h. es
     ist wirklich noetig, dass man 'os.operator<< (t)' bzw.
     'os.operator>> (t)' schreiben kann), aber es gibt fuer den
     Compiler Member-Templates (OK, ist nur ein Scherz: soweit ich
     weiss gibt es kaum/keinen Compiler, die/der bereits
     Member-Templates unterstuetzen/unterstuetzt). Insbesondere spricht
     die Existenz von 'fstream' dagegen, dass der verwendete Compiler
     in der Naehe des DWP ist...
  3. Wie 2., aber es gibt keine Member-Templates (das ist der Fall, der
     wirklich uebel ist: Da fuehrt kein Weg an einem, IMHO, schlechten
     Design dran vorbei)

Im 1. Fall geht noch alles ganz gut und es reichen zwei zusaetliche
Member-Funktionen von 'ObjectStream' und zwei Templates, um schonmal
die vorhanden Operatoren (fuer alle Klassen mit Inserter/Extractor) zu
verwenden:

  ostream &ObjectStream::out() { return i_out; }
  istream &ObjectStream::in()  { return i_in; }

  template <class T>
  inline ObjectStream &operator<< (ObjectStream &os, T const &t)
  {
    os.out() << t;
    return os;
  }

  template <class T>
  inline ObjectStream &operator>> (ObjectStream &os, T &t)
  {
    os.in() >> t;
    return os;
  }

It's magic, aber das reicht schonmal, um die vorhanden Operatoren fuer
'[io]stream' zu benutzen! Wenn auch noch die Manipulator verwendet
werden muessen, braucht man vielleicht noch ein paar Templates mehr,
aber die sind nach genau dem selben Stil zu schreiben, wie die beiden
oben. Die 'inline's sind uebrigens nicht fuer Effizienz, sondern um
Probleme mit der Instantiierung zu vermeiden.

Fall 2, wenn er denn tatsaechlich eintreten sollte, ist vielleicht noch
schoener, weil man die Member 'in()' und 'out()' nicht braucht,
andererseits ist man vermutlich trotzdem gut beraten, sie zur
Verfuegung zu stellen... Der Unterschied zu Fall 1 ist lediglich, dass
man wirklich Member hat (also so, wie es anscheinend gefordert ist):

  template <class T>
  inline ObjectStream &ObjectStream::operator<< (T const &t)
  {
    i_out << t;
    return *this;
  }

  template <class T>
  inline ObjectStream &ObjectStream::operator>> (T &t)
  {
    i_in >> t;
    return *this;
  }

Selbstverstaendlich muss man die Member passend in der Klasse
deklarieren und fuer Manipulatoren gilt das selbe wie im Fall 1.

Fall 3: "Das waer ihr Preis gewesen..." heisst es hier, denn das ist
die Niete (und bei dem was ich aus Deinem Artikel lese und dem Stand
der Compiler, das was der Fall ist). Je nachdem, ob "etc." in der
Spezifikation fuer "built-in Typen" oder fuer "alle Typen mit
Extractor/Insertor" steht, ist diese "Loesung" praktikabel oder eben
gerade nicht: Da die Operatoren einen 'ObjectStream' liefern sollen,
kann man sie nicht erben, sondern muss einen Wrapper schreiben. Da sie
Member sein sollen, muessen sie in die Klasse und da keine
Member-Templates unterstuetzt werden, muss man jeden Operator explizit
schreiben, so trivial er auch ist: Im Prinzip genau das, was beim Fall
2 passiert, nur statt 'T' jeden einzelnen Typ, z.B.:

  inline ObjectStream &ObjectStream::operator<< (char c)
  {
    i_out << c;
    return os;
  }

  inline ObjectStream &ObjectStream::operator>> (char &c)
  {
    i_in >> c;
    return os;
  }

"etc.", fuer die eingebauten Typen immerhin schon 16 mal (das ist die
Anzahl der entsprechenden Operatoren, die ich fuer 'ostream' gezaehlt
habe; allerdings sind nicht alle nur fuer die built-in Typen). Fuer
alle Typen mit Inserter/Extractor (diesen Fall werde ich im folgenden
als "Super-GAD", "groesster anzunehmender Designfehler" bezeichnen;
warum siehe unten), beliebig viele!

Falls der Super-GAD eingetreten ist, tun sich alle Beteiligten einen
grossen Gefallen, wenn die Spezifikation, etwa zum Fall 1 hin,
geaendert wird: Fuer Dich heisst das, dass Du nicht jetzt schonmal
massenhaft dieser trivialen Operatoren schreiben musst (auch wenn man
das zu einem Makro macht, heisst das, dass man massenhaft Typnamen
eintasten muss). Fuer die Benutzer ist es aber auch von Vorteil, wenn
dieser Ansatz nicht benutzt wird: Fuer jede neue Klasse mit
Extractor/Inserter muesste ein neues Paar von Funktionen hinzugefuegt
werden. Das bedeutet, dass ALLE Files, in denen 'ObjectStream' benutzt
wird, recompiliert werden muessen (nicht wirklich, aber alles andere
fuehrt normalerweise zu einem nicht handhabbaren Pfusch). Das ist nicht
akzeptabel, insbesondere, da es eine bessere Loesung gibt (etwa Fall 1,
bzw., IMHO, noch besser ganz ohne 'ObjectStream'). Daher halte ich,
falls wirklich dieser Fall da ist, es fuer einen Designfehler, wie er
kaum noch groesser sein koennte. Im uebrigen halt ich Fall 3, den nicht
Super-GAD Teil, (und auch, wenn man ganz ehrlich ist, Fall 2) fuer eine
GAD, halt einen, bei dem man den Schaden begrenzen kann (im Gegensatz
zum Super-GAD, wo der Designfehler unbegrenzt viel Arbeit nach sich
zieht).

Wie dem auch sein, alle anderen Operatoren lassen sich zu
'ObjectStream' hinzufuegen, wie man gerade lustig ist: Als Member oder
als nicht-Member. Wenn man sie als nicht-Member hinzufuegt, sollte man
sich ueberlegen, ob man sie nicht gleich zu 'ostream' bzw. 'istream'
hinzufuegt: Warum einschraenken, wenn man es auch allgemein schreiben
kann...

Das fuehrt mich nochmal zurueck, zu dem was ich bereits in meinem
vorherigen Artikel geschrieben habe: Spezifikation hin, Spezifikation
her. Egal. Wenn die Spezifikation Mist ist und es keine wirklich guten
Grund gibt, sie beizubehalten, dann muss sie eben geaendert werden.
Ein Grund wie "Das steht jetzt aber schon in der Spezifikation" ist
uebrigens kein wirklich guter Grund:  Designfehler muessen raus, so
schnell wie moeglich (nicht "koste es was es wolle", denn das tut es,
wenn der Designfehler drin bleibt :-). Damit man mit einem Designfehler
lebt, muss es schon wirklich triftige Gruende geben, denn meist sind
die Kosten hoeher, Problem aufgrund schlechten Designs zu umgehen, als
das Design zu aendern. Insbesondere, wenn noch keiner oder wenig Code
existiert. Da Du Dich nicht dazu geaeussert hast, warum 'ObjectStream'
noetig ist (ausser, dass es in der Spezifikation steht, was fuer mich
ein Ungrund ist, wenn es Argumente dagegen gibt), gehe ich jetzt davon
aus, dass 'ObjectStream' nur dazu da ist, 'os << object' bzw. 'os >>
object' ('os' wieder ein 'ObjectStream' und 'object' ein 'Object' oder
so) samt Verwandten zu ermoeglichen. Das ist z.B. ein ganz schlechter
Grund und ein grober Schnitzer im Design => Spezifikation aendern. Das
macht man anders:

  istream &operator>> (istream &in, Object &o)
  {
    // ... was fuer 'Object' noetig ist
    return in;
  }

  ostream &operator<< (ostream &out, Object const &o)
  {
    // ... was fuer 'Object' noetig ist
    return out;
  }

Naemlich so.

: Gibt es eine andere Art, die vordefinierten Operatoren <<, >> und die
: File-spezifischen Eigenschaften von fstream wiederzuverwenden und 
: neue Stream-Operatoren hinzuzufuegen, als von einer Stream-Klasse
: zu vererben?

Sicher. Siehe oben. Das ist ein Beispiel, wie man es machen kann. Man
kann auch die file-spezifischen Eigenschaften von 'fstream' verwenden,
ohne 'fstream' zu verwenden: Alles was an 'fstream' file-spezifisch
ist, steckt in der Klasse 'filebuf'.  Alles was 'fstream' (bzw. nach
Standard ja nur '[io]fstream') macht, ist seiner Basisklasse bzw.
seinen Basisklassen einen Pointern zu einem 'filebuf' beim
Konstruktoraufruf mitzugeben (die vier Service-Funktionen sind schnell
selbstgebaut). Das koennte man z.B. verwenden, wenn man binaer anstatt
formatiert schreiben will. Allerdings muesste man dann die ganzen
anderen Operatoren wirklich selbst schreiben, da es im Standard keine
Methode gibt, Objekte binaer zu schreiben...
--
dietmar.kuehl@uni-konstanz.de
http://www.informatik.uni-konstanz.de/~kuehl
I am a realistic optimist - that's why I appear to be slightly pessimistic
Technical Details
Message-ID: <4ie1up$500@news.BelWue.DE>
Path: arc1-ca.pugleaf.net!news.belwue.de!uzwil!kuehl
References: <31486437.41C6@studi.epfl.ch> <4ia5sb$cfk@news.BelWue.DE> <3149644C.41C6@studi.epfl.ch>

Show Full Headers
Complete Article Headers
Path: arc1-ca.pugleaf.net!news.belwue.de!uzwil!kuehl
From: kuehl@uzwil.informatik.uni-konstanz.de (Dietmar Kuehl)
Newsgroups: de.comp.lang.c++
Subject: Re: Probleme beim Vererben von fstream
Date: 16 Mar 1996 09:34:17 GMT
Organization: Fakultät für Mathematik und Informatik
Message-ID: <4ie1up$500@news.BelWue.DE>
References: <31486437.41C6@studi.epfl.ch> <4ia5sb$cfk@news.BelWue.DE> <3149644C.41C6@studi.epfl.ch>
Reply-To: dietmar.kuehl@uni-konstanz.de
NNTP-Posting-Host: uzwil.informatik.uni-konstanz.de
X-Newsreader: TIN [version 1.2 PL2]