[X] Benutzerdefinierte Manipulatoren
-
Inhalt
- Einleitung
- Manipulatoren ohne Argumente
- Ein Generator für HTML-Manipulatoren ohne Argumente
- Ein Generator für HTML-Manipulatoren mit einem Argument
- Ereignisgesteuerte Manipulatoren
- Referenzen
1 Einleitung
Manchmal benötigen wir für einen benutzerdefinierten Datentypen eine Ein- oder Ausgabefunktionen.
struct name { const char * christian_; const char * family_; };
Um die Struktur auszugeben, würden wir in C wahrscheinlich eine Funktion print_name definieren.
void print_name(name * n) //Ausgabe in C { printf("%s %s\n",n->christian_,n->family_); }
In C++ haben wir noch andere Alternativen. Die C++-IO-Bibliothek ist erweiterbar, da sie auf Überladung, und nicht wie in C auf Formatstrings basiert.
Wir können jedes Objekt obj mit der Anweisungcout << "Objekt = " << obj;
ausgeben. Wir müssen für benutzerdefinierte Datentypen lediglich eine Funktion mit der Signatur
std::ostream& operator<<(std::ostream&, const obj &)
definieren. Ich finde diese Schreibweise sehr angenehm, da immer derselbe Mechanismus verwendet wird. Der Ausgabe-Operator für die Struktur name könnte z.B. folgendermaßen implementiert werden.
std::ostream & operator << (std::ostream & o_s, const name & n) //Ausgabe Operator in C++ { return o_s << std::string(n.christian_) .append(1,' ') .append(n.family_); }
Ein Objekt vom Typ name kann dann nach oben genanntem Muster ausgegeben werden.
int main(int argc, char* argv[]) { name n = {"Bjarne","Stroustrup"}; //----------- Ausgabe in C ----------- print_name(&n); printf("\n"); //----------- Ausgabe in C++ --------- cout << n << endl; }
Manchmal stellt sich allerdings auch noch die Frage, wie ein Objekt dargestellt werden soll.
- Soll zuerst der Nachname und dann der Vorname ausgegeben werden?
- Wird die Zahl hexadezimal, oktal oder dezimal dargestellt?
- Erfolgt die Längenangabe in m, cm oder inch?
- Möchten Sie das Datum "04.07.1966" nicht lieber im Format "4. Juli 1966" darstellen?
- Muss die komplexe Zahl 2 + 3i auch von einem Elektrotechniker verstanden werden? Dann schreiben Sie besser 2 + 3j.
Am besten überlassen wir die Entscheidung demjenigen, der unsere Datentypen verwendet. Um Formate zu steuern, werden meistens Manipulatoren verwendet. Sie vereinfachen die Verwendung der IO-Streams in C++. Um z.B. eine hexadezimale Zahl mit mindestens 4 Stellen auszugeben, die notfalls links mit Nullen aufgefüllt wird schreibt man:
cout << "0x" << std::hex //hexadezimal << std::uppercase //Großbuchstaben << std::setw(4) //Mindestens 4 Zeichen ausgeben << std::setfill('0') //Zeichen mit '0' auffüllen << std::right //rechtsbündig << 123 //Hexadezimal 0x007B << std::setfill(' '); //Zeichen beim nächsten mal wieder mit ' ' auffüllen
C++ bietet die Möglichkeit, diesen Mechanismus für selbstgeschriebene Klassen zu nutzen.
2 Manipulatoren ohne Argumente
Jedes Objekt der Klasse ios_base besitzt ein erweiterbares Array, auf das mit der Memberfunktion iword zugegriffen werden kann. Das Array wird automatisch erweitert, sobald iword mit einem Argument aufgerufen wird, das den bisherigen Wert übersteigt. Wenn ein fester Wert für das Argument von iword verwendet wird, kann die allozierte Variable wie ein benutzerdefiniertes Formatflag verwendet werden. Welchen Wert nehmen wir aber nun für das Argument von iword? Wir müssen irgendwie vermeiden, dass zwei Klassen denselben Speicherplatz für Ihre Formatflags verwenden.
class ios_base { //... inline long& iword(int n); static int xalloc() throw(); //... }
Die Memberfunktion xalloc inkrementiert bei jedem Aufruf den Wert einer internen statischen Integer-Variable. Wir können also den Rückgabewert dieser Funktion dazu verwenden, einen eindeutigen Index für ein Formatflag zu erhalten.
long & my_format_flag(std::ios_base & s) { static int my_index = std::ios_base::xalloc(); return s.iword(my_index); } long get_my_format_flag(std::ios_base & s) { return my_format_flag(s); } void set_my_format_flag(std::ios_base & s, long n) { my_format_flag(s) = n; }
Bem.: Die statische Variable my_index wird einmalig, beim ersten Aufruf von my_format_flag initialisiert.
Um einem benutzerdefinierten Formatflag einen Wert zuzuweisen, kann z.B. eine Enumeration verwendet werden.
struct lenght_s { double millimeter; }; enum my_length_format_flags { length_in_mm = 0, length_in_cm = 1, length_in_dm = 2, length_in_m = 3, length_in_inch = 4, }; static void set_length_format(std::ios_base & s, my_length_format_flags len_fmt) { my_format_flag(s) = len_fmt; }
Ein mit iword alloziertes Element wird mit 0 initialisiert. Deswegen ist length_in_mm der Default-Wert für das benutzerdefinierte Formatflag. Die Definition von Manipulatoren ist jetzt nicht mehr besonders schwierig.
namespace length { std::ostream & mm (std::ostream & os) { set_length_format(os,length_in_mm); return os; } std::ostream & cm (std::ostream & os) { set_length_format(os,length_in_cm); return os; } //... }
Bem.: Die C++-Standard-Bibliothek enthält eine überladene Operatorfunktion, die als Argumente ein ostream-Objekt sowie einen Zeiger auf eine Funktion erwartet.
Jetzt fehlt nur noch die Implementierung des Ausgabeoperators.
std::ostream& operator<<(std::ostream & os, const length_s & length) { std::ostringstream oss; //stringstream if(length_format(os) == length_in_cm) oss << length.millimeter/10.0 << " cm"; else if(length_format(os) == length_in_dm) oss << length.millimeter/100.0 << " dm"; //... else if(length_format(os) == length_in_inch) oss << length.millimeter/25.4 << " inch"; else oss << length.millimeter << " mm"; return os << oss.str(); }
Bem.: Das formatierte Objekt wird zunächst zwischengespeichert und anschließend mit einer Anweisung ausgegeben. Da häufig weitere Manipulatoren auf den Stream einwirken, vermeiden wir dadurch unerwünschte Seiteneffekte.
Nachdem die Operator-Funktion << für die Struktur length überladen wurde, können wir unsere Manipulatoren anwenden:
int main() { length_s len = {12345}; cout << length::mm << len << endl; cout << length::cm << len << endl; }
Die Ausgabe liefert das erwartete Ergebnis.
12345 mm 1234.5 cm
3 Ein Generator für HTML-Manipulatoren ohne Argumente
Als nächstes möchte ich parameterlose Manipulatoren betrachten, die nicht zur Formatierung eines Objekts eingesetzt werden, sondern einen Text in einen Stream einfügen. Solche Manipulatoren werden in der Literatur manchmal auch als Inserter bezeichnet. Betrachten wir einige Inserter die für die Ausgabe von HTML-Dokumenten sinnvoll sind.
//----------------------------------------- std::ostream & table(std::ostream & o_s) { return o_s << "\n<table>" } //----------------------------------------- std::ostream & tr(std::ostream & o_s) { return o_s << "\n<tr>" }
Die Definition solcher Manipulatoren ist Fleißarbeit. Die Arbeit könnte leicht durch einen Generator erledigt werden. In diesem Fall benutze ich den Präprozessor als Generator. Es gibt aber einfach zu viele HTML-Tokens, um alle HTML-Token-Manipulatoren per Hand auszuprogrammieren. Eine alternative Technik wäre z. B die Copy-Paste-Programmierung, deren Vorzüge sicher in jedem guten C++-Buch beschrieben werden. Durch den geschickten Einsatz von Makros können wir mit einer Zeile Code mehrere Manipulatoren definieren. Lassen wir uns also auf ein paar hässliche Makros ein.
Ich habe die Makros in der Headerdatei html_manip_macros.h definiert.
#ifndef __html_manip_macros_21_06_2006_hdr__ #define __html_manip_macros_21_06_2006_hdr__ //----------------------------------------------------------------------------------------------------------------------------------- //Makro zum generieren eines HTML-Manipulator #define MAKE_HTML_MANIPULATOR(name_space,name,start_token,token,end_token) \ namespace { \ namespace html { \ namespace name_space { \ std::ostream & name(std::ostream & o_s) \ { \ return o_s << ##start_token#token##end_token; \ } \ }}} //namespace unbenannt, name_space und html abschließen //----------------------------------------------------------------------------------------------------------------------------------- #define MAKE_HTML_MANIP_SNT(name_space,name,start_token,end_token) MAKE_HTML_MANIPULATOR(name_space,name,start_token,name,end_token) #define MAKE_HTML_MANIP_BEG(name) MAKE_HTML_MANIP_SNT(beg,name,"\n<" ,">") #define MAKE_HTML_MANIP_END(name) MAKE_HTML_MANIP_SNT(end,name,"\n</",">") #define MAKE_HTML_MANIP_BETWEEN(name) MAKE_HTML_MANIPULATOR(between,name,"\n</",name##>\n<##name,">") #define MAKE_HTML_MANIP_OPEN(name) MAKE_HTML_MANIP_SNT(open,name,"\n<"," ") //----------------------------------------------------------------------------------------------------------------------------------- #define MAKE_HTML_MANIP_BEG_END(name) MAKE_HTML_MANIP_BEG(name) MAKE_HTML_MANIP_END(name) #define MAKE_HTML_MANIP_BEG_END_BETWEEN(name) MAKE_HTML_MANIP_BEG_END(name) MAKE_HTML_MANIP_BETWEEN(name) #define MAKE_HTML_MANIP_BEG_END_BETWEEN_OPEN(name) MAKE_HTML_MANIP_BEG_END_BETWEEN(name) MAKE_HTML_MANIP_OPEN(name) #define MAKE_HTML_CLOSE_TOKEN MAKE_HTML_MANIPULATOR(close,token,"",,">") //----------------------------------------------------------------------------------------------------------------------------------- #endif
Bem.: Das Kürzel SNT bedeutet, dass der Name des Manipulators und des Tokens identisch sind (same name as token)
- Der HTML-Manipulator wird in einen unbenannten Namensraum definiert um Konflikte mit Mehrfach-Definitionen zu vermeiden. Alternativ hätten wir den Manipulator auch inlinen können. Dies ist aber nicht sehr sinnvoll, weil beim Aufruf des Manipulators ein Funktionspointer dereferenziert werden muss. Das bedeutet natürlich, dass die Funktion im Objekt-Code vorhanden sein muss. Genau das sollte aber bei der inline-Deklaration vermieden werden, da der Aufruf einer Inline-Funktion durch den Funktionsrumpf ersetzt werden soll.
- Als nächstes wird der Namensraum html im unbenannten Namensraum verschachtelt
- Der nächste Namensraum ist frei wählbar. Sinn und Zweck dieses Namensraums ist es zwischen verschiedenen Arten von Tokens zu unterscheiden (z. B. <table> und </table>).
- Die Präprozessor-Anweisung # wandelt den übergebenen Ausdruck in einen String um.
- Die Angabe von ## ist notwendig um die Ausdrücke verbinden zu können.
- Vorsicht: Nach einer Zeilenumbruchsanweisung ** muss zwingend ein Carrage-Return eingefügt werden (kein Leerzeichen).
Die Vorarbeit war zwar etwas lästig, aber dafür können wir nun in der Datei html_manip.h massenweise Manipulatoren definieren. MAKE_HTML_MANIP_BEG_END_BETWEEN_OPEN generiert je einen HTML-Manipulatoren im Namensraum beg, end, between und open. Die Namensräume sind so gewählt, dass sie ihrer Bedeutung entsprechen.
#ifndef __html_manip_21_06_2006_hdr__ #define __html_manip_21_06_2006_hdr__ #include "html_manip_macros.h" MAKE_HTML_MANIP_BEG_END_BETWEEN_OPEN(table) MAKE_HTML_MANIP_BEG_END_BETWEEN_OPEN(tr) MAKE_HTML_MANIP_BEG_END_BETWEEN_OPEN(td) MAKE_HTML_MANIP_BEG_END_BETWEEN_OPEN(h1) MAKE_HTML_MANIP_BEG_END_BETWEEN_OPEN(h2) MAKE_HTML_CLOSE_TOKEN #endif
#include <iostream> #include "html_manip.h" using namespace std; int main() { cout << html::beg::h1 << " caption 1" << html::end::h1 << html::beg::h2 << " caption 2" << html::end::h2 << html::open::table << "border=\"1\" cellspacing=\"1\" cellpadding=\"1\" align=\"center\"" << html::close::token << html::beg::tr << html::beg::td << " row 1 col 1" << html::between::td << " row 1 col 2" << html::end::td << html::end::tr << html::end::table << endl ; }
Der Code in der main-Funktion ähnelt, in gewisser Weise, der ausgegebenen HTML-Datei.
<h1> caption 1 </h1> <h2> caption 2 </h2> <table border="1" cellspacing="1" cellpadding="1" align="center"> <tr> <td> row 1 col 1 </td> <td> row 1 col 2 </td> </tr> </table>
Die Attribute des HTML-Tokens table habe ich zunächst als String eingefügt. Im nächsten Kapitel möchte ich zeigen, wie man sie in Manipulatoren verpacken kann.
4 Ein Generator für HTML-Manipulatoren mit einem Argument
Beginnen wir mit einem einfachen Beispiel. Das Attribut border soll als Manipulator implementiert werden, dem ein Argument übergeben wird.
Am einfachsten ist es, eine Klasse border zu definieren, die mit Hilfe eines befreundeten Ausgabeoperators in einen Stream geschrieben werden kann.#include <iostream> #include <sstream> using namespace std; //-------------------------------------------------------------------------- class border { friend std::ostream & operator<<(std::ostream &, const border &); private: const unsigned int width_; public: explicit border(unsigned int width) :width_(width) {} //ctor }; //--------------------------------------------------------------------------
Die Klasse besitzt lediglich einen Konstruktor und die friend-Deklaration. Die Ausgabefunktion muss natürlich auch noch geschrieben werden.
//-------------------------------------------------------------------------- std::ostream & operator<<(std::ostream & o_s, const border & b) { ostringstream oss; oss << "border = \"" << b.width_ << "\""; return o_s << oss.str(); } //--------------------------------------------------------------------------
Das wars auch schon. Der Manipulator kann verwendet werden.
//-------------------------------------------------------------------------- int main() { cout << border(1) << endl; } //--------------------------------------------------------------------------
Wir können jetzt die Manipulatoren cellspacing, cellpadding, align, usw. der Reihe nach implementieren.
Da immer das gleiche Implementierungsmuster verwendet wird, bietet es sich an, eine Template-Klasse zu schreiben.template <unsigned long ID,typename Parm1> struct base_attribute { typedef typename base_attribute<ID,Parm1> me; private: Parm1 val_; public: base_attribute(Parm1 val) :val_(val) {} friend std::ostream& operator<<(std::ostream & o_s, const base_attribute<ID,Parm1> & obj); };
Mit einer Typdefinition wird dann der Name für unseren Manipulator erzeugt.
typedef base_attribute<0,unsigned long> border;
Ein Makro erleichtert wieder die Generierung der Manipulatorern.
#define MAKE_HTML_ATTRIB_MANIP(ID,Parm,name,token) \ namespace html { \ namespace attrib { \ typedef base_attribute<ID,Parm> token; \ std::ostream & operator<<(std::ostream & o_s, const base_attribute<ID,Parm> & obj) \ { \ std::ostringstream oss; \ oss << #token << " = \"" << obj.val_ << "\" "; \ return o_s << oss.str(); \ } \ }} #define MAKE_HTML_ATTRIB_MANIPULATOR(Parm,name,token) MAKE_HTML_ATTRIB_MANIP(__LINE__,Parm,name,token) #define MAKE_HTML_ATTRIB_MANIPULATOR_SNT(Parm,name) MAKE_HTML_ATTRIB_MANIPULATOR(Parm,name,name)
Bem.: Die ID ist lediglich ein Dummy-Parameter um beliebig viele Typen generieren zu können. Da der konkrete Wert keine Rolle spielt habe ich das __LINE__-Makro als Übergabe-Parameter verwendet.
Die Manipulatoren für die Ausgabe der HTML-Attribute können nun generiert werden.
MAKE_HTML_ATTRIB_MANIPULATOR_SNT(unsigned long,border) MAKE_HTML_ATTRIB_MANIPULATOR_SNT(unsigned long,cellspacing) MAKE_HTML_ATTRIB_MANIPULATOR_SNT(unsigned long,cellpadding) MAKE_HTML_ATTRIB_MANIPULATOR_SNT(unsigned long,bgcolor) MAKE_HTML_ATTRIB_MANIPULATOR_SNT(const char *,align)
Attributwerte vom Typ *const char ** definiere ich im Namensraum html::attrib::value.
namespace html { namespace attrib{ namespace value { static const char * center = "center"; static const char * justify = "justify"; static const char * left = "left"; static const char * right = "right"; }}}
Die HTML-Attribute für die Tabelle
<table border="1" cellspacing="1" cellpadding="1" align="center">
können jetzt mit Hilfe der Manipulatoren geschrieben werden.
cout << html::open::table << html::attrib::border(1) << html::attrib::cellspacing(1) << html::attrib::cellpadding(1) << html::attrib::align(html::attrib::value::center); << html::close::token << endl;
5 Ereignisgesteuerte Manipulatoren
HTML-Token werden im Allgemeinen, abhängig von der Dokumenten-Hierarchie eingerückt. Es stellt sich die Frage, ob diese Aufgabe von einem Manipulatoren erledigt werden kann. Im Unterschied zu den bisherigen betrachteten Fällen, müssten wir den Stream permanent überwachen. Ich war überrascht, dass ich bei meinen Internet-Recherchen kaum Informationen zu diesem Thema finden konnte. Dabei stellt sich hier eine ganz grundsätzliche Frage. Kann ich einen Manipulator wie z.B. std::hex implementieren, der sich auf alle in den Stream eingefügten Integer-Zahlen auswirkt? Wie wir gleich sehen werden, müssen wir einen Streambuffer erstellen und mit dem Stream verknüpfen. Die Frage könnte also auch anders gestellt werden. Ist es sinnvoll, einem Manipulator die Verantwortung für die Kopplung von Buffer und Stream zu übertragen?
Widmen wir uns aber wieder unserer Aufgabenstellung. Um auf einen Zeilenumbruch reagieren zu können, müssen wir eine von std::basic_streambuf<char_type,traits> abgeleitete Klasse implementieren, die ich im Folgenden als Indent-Buffer-Klasse bezeichne. Da die Basisklasse die virtuelle Methode overflow definiert, die vor jeder Schreiboperation aufgerufen wird, können wir durch Überschreiben der Methode die Ausgabe der Zeichen steuern. Die Hauptaufgabe des Manipulators besteht darin, den Buffer an den übergebenen Stream zu koppeln.Die Indent-Buffer-Klasse besitzt die von std::basic_streambuf übernommenen Template-Parameter, sowie einen zusätzlichen Template-Parameter um das Füllzeichen zu spezifizieren.
#include <streambuf> template <typename char_type, char_type fill_char = ' ', typename traits = std::char_traits<char_type> > struct basic_indent_buf : public std::basic_streambuf<char_type,traits> { //... std::basic_streambuf<char_type,traits> * m_sbuf; int count_; //Anzahl der Füllzeichen bool set_; //Flag ob Füllzeichen bereits eingefügt wurden //... }
Die Membervariable m_sbuf zeigt auf den Buffer des Streams, der mit unserem Manipulator verknüpft ist. Die Einrückungstiefe wird in count_ gespeichert. Außerdem gibt es noch eine Variable, die als Zustandsflag verwendet wird. Die Implementierung von overflow ist nicht besonders schwierig. Sobald ein Zeilenumbruch erkannt wird, wird das Zustandsflag gesetzt. Damit haben wir ein Kriterium, ob bei der nächsten Schreiboperation die Einrückung der Zeile erfolgen muss.
//--------------------------------------------------------------------------------- virtual int_type overflow(int_type c = traits::eof()) //überschreibt std::streambuf::overflow // { if (traits_type::eq_int_type(c, traits_type::eof())) return m_sbuf->sputc(c); if (set_) { //n-mal Füllzeichen einfügen std::fill_n(std::ostreambuf_iterator<char_type>(m_sbuf), count_, fill_char); set_ = false; } if (traits_type::eq_int_type(m_sbuf->sputc(c), traits_type::eof())) return traits_type::eof(); if (traits_type::eq_int_type(c, traits_type::to_char_type('\n'))) //beim Zeilenumbruch wird set_ = true; //das Zustandsflag gesetzt return traits_type::not_eof(c); } //---------------------------------------------------------------------------------
Damit wären die wesentlichen Elemente der Indent-Buffer-Klasse implementiert. Wir können den Indent-Buffer bereits testen.
Um die Ausgabe besser kontrollieren zu können, definieren wir einen Indent-Buffer, der '+'-Zeichen zum Auffüllen verwendet.//... typedef format::basic_indent_buf<char,'+', std::char_traits<char> > plus_indentbuf; //Intend-Buffer mit '+' als Füllzeichen //... int main() { plus_indentbuf sbuf(plus_indentbuf(cout.rdbuf())); //Indent-Buffer Objekt anlegen cout.rdbuf(&sbuf); //Buffer und Stream verknüpfen sbuf.indent(10); //Einrückungstiefe setzen cout << "Die Einrueckungstiefe" << endl << "betraegt" << endl << sbuf.indent() << endl; }
Die Ausgabe zeigt, dass nach jedem Zeilenumbruch eine Einrückung mit 10 '+'-Zeichen erfolgt.
++++++++++Die Einrueckungstiefe ++++++++++betraegt ++++++++++10.
Für den Manipulator benötigen wir noch eine kleine Hilfsstruktur.
struct indent { indent(int i) : indent_(i) {} int indent_; };
Im Funktionsrumpf des indent-Manipulators wird zunächst überprüft, ob bereits ein indentbuf-Objekt mit dem Stream verknüpft ist. Falls nicht wird ein dynamisch erzeugtes Indent-Buffer-Objekt installiert. Die Adresse des Ausgabestreams speichern wir mit Hilfe der Methode pword, die uns Zugriff auf ein erweiterbares Array mit void*-Pointer gewährt.
//--------------------------------------------------------------------------------- std::ostream& operator<< (std::ostream& out, format::indent const& ind) { format::indentbuf* sbuf = dynamic_cast<format::indentbuf*>(out.rdbuf()); //stream pointer lesen if (!sbuf) //wenn streambuffer pointer noch nicht exitstiert { sbuf = new format::indentbuf(out.rdbuf()); //muss er erzeugt werden. out.pword(index) = &out; //pword pointer auf Adresse von out setzen out.register_callback(callback, 0); //callback Funktion registrieren out.rdbuf(sbuf); //out stream buffer pointer setzen } sbuf->indent(ind.indent_); //indent spacing setzen return out; } //---------------------------------------------------------------------------------
Da der Buffer dynamisch alloziert wurde, müssen wir uns darum kümmern, dass der Speicher wieder freigegeben wird. Zu diesem Zweck registrieren wir die Funktion callback. Sie wird aufgerufen sobald das Indent-Buffer-Objekt zerstört wird.
//--------------------------------------------------------------------------------- static int const index = std::ios_base::xalloc(); //--------------------------------------------------------------------------------- void callback(std::ios_base::event ev, std::ios_base& ios, int) { if (ev == std::ios_base::erase_event) { std::ostream* out = static_cast<std::ostream*>(ios.pword(index)); format::indentbuf* sbuf = dynamic_cast<format::indentbuf*>(out->rdbuf()); out->rdbuf(sbuf->sbuf()); delete sbuf; } } //---------------------------------------------------------------------------------
Leider hat die gezeigte Implementierung mindestens zwei schwerwiegende Mängel. Sie treten zu Tage, sobald der Zustand eines Stream mit installiertem Indent-Buffer kopiert wird.
//--------------------------------------------------------------------------------- cerr.copyfmt(cout); //---------------------------------------------------------------------------------
Durch den Aufruf von copyfmt werden die im pword-Array gespeicherten Pointer-Adressen kopiert. Da außerdem alle Callback-Funktionen der Kopiervorlage im Ziel-Stream registriert werden, wird der delete-Operator für jede kopierte Speicheradresse des Buffer aufgerufen, was geradewegs ins Nirwana führt.
Das zweite Problem ist, dass beim Kopieren ein Ressourcen-Leck entsteht, falls im Ziel-Stream bereits ein indent-Buffer installiert ist. Da die Adresse des indent-Buffer-Objekt überschrieben wird kann auf das Objekt nicht mehr zugegriffen werden.
Offensichtlich ist das pword-Array eher dafür ausgelegt, Zeiger auf statische Objekte (z.B. Funktionspointer, etc.) zu speichern. Für das Speichern von dynamisch allozierten Objekten ist das Array auf jeden Fall nicht zu gebrauchen. Es spricht alles dafür, den Manipulator sowie die callback-Funktion einem Redesign zu unterwerfen. Zunächst benötigen wir einen Manager, der die indent-Buffer-Objekte unabhängig von der IO-Library verwalten kann.template <typename char_type, typename traits> class basic_buff_mng { typedef std::ios_base bos; typedef std::basic_streambuf<char_type,traits> bsb; typedef std::map<bos*,bsb*> table_type; typedef typename table_type::value_type value_type; typedef typename table_type::const_iterator const_iterator; static int instances_; table_type list; //... }
Die statische Klassenvariable instances_ speichert, wie viele Instanzen des Buffer-Managers existieren. Der Wert wird im Konstruktor inkrementiert und im Destruktor dekrementiert. Damit wir die Anzahl der Instanzen jederzeit abfragen können, wird eine statische Methode instances zur Verfügung gestellt.
static size_t instances() { return instances_; }
Der Buffer-Manager soll als Singleton implementiert werden, d.h. es darf nur eine einzige Instanz im laufenden Programm geben.
static basic_buff_mng<char_type, traits> * instance() { static basic_buff_mng<char_type, traits> ibm; return &ibm; }
Bei der Instanzzählung interessiert uns lediglich, ob noch eine Instanz existiert. Es kann durchaus passieren, dass die Buffer-Manager-Klasse zerstört wird, obwohl noch Streams wie z.B. std::cout existieren. Das bedeutet, dass ein erase_even event, das am Programmende aufgerufen wird nicht mehr behandelt werden kann, wenn die Instanz des Managers bereits zerstört ist. Aus diesem Grund gibt der Manager beim Aufruf des Destruktors alle verbleibenden Elemente der Liste frei.
~basic_buff_mng() //dtor { --instances_; std::for_each(list.begin(),list.end(),DeleteSecond()); }
Das Funktionsobjekt DeleteSecond ermöglicht die Zusammenarbeit mit der im Header <algorithm> definierten Funktion for_each.
struct DeleteSecond ///< Funktionsobjekt zum Löschen von dynamisch allozierten Objekten { template <typename T,typename U> inline void operator() (std::pair<T,U> & object) const { delete object.second; } };
Beim Aufruf der Methode erase wird ein Element aus der Liste entfernt. Zuvor muss der Speicher für das dynamisch allozierte Stream-Buffer-Objekt freigegeben.
bool erase(std::ios_base & io_s) { delete get(io_s); return(list.erase(&io_s) == 1); }
Das Einfügen von Elementen erfolgt mit der Methode insert. Da der Manager über keine konkreten Typinformationen verfügt, muss sich der Aufrufer um die Erzeugung der Indent-Buffer-Objekte kümmern.
bool insert(bos & o_s, bsb * b_s) { return list.insert(value_type(&o_s, b_s)).second; }
Jetzt kann die callback-Funktion geschrieben werden.
void callback(std::ios_base::event ev, std::ios_base& ios, int x) { if(buffer_mng::instances()) //existiert das Singleton-Objekt noch? { if (ev == std::ios_base::erase_event) { buffer_mng::instance()->erase(ios); } else if(ev == std::ios_base::copyfmt_event) { if(std::ostream & o_s = dynamic_cast<std::ostream &>(ios)) o_s << format::indent(get_indent(ios)); } } }
Die Events werden nur behandelt, wenn die Instanz des Managers noch existiert. Das Löschen des Buffers erledigt der Manager. Beim Kopieren eines Streams wird der Indent-Manipulator aufgerufen, der sich um die Erzeugung der Indent-Buffer-Objekte kümmert. Die Funktion get_indent liefert die in iword gespeicherte Einrückungstiefe des kopierten Streams. Jetzt fehlt nur noch der Ausgabe-Manipulator für die indent-Klasse.
template <typename char_type, typename traits> std::ostream& operator<< (std::basic_ostream<char_type,traits> & o_s, format::indent const& ind) { format::indentbuf* sbuf = dynamic_cast<format::indentbuf*>(o_s.rdbuf()); //stream pointer lesen if (!sbuf) //wenn streambuffer pointer noch nicht exitstiert { std::ios_base & ios(dynamic_cast<std::ios_base &>(o_s)); sbuf = install_buffer(o_s); o_s.register_callback(callback, 0); //callback Funktion registrieren } set_indent(o_s,ind.indent_); sbuf->indent(ind.indent_); //indent spacing setzen return o_s; }
Die Hilfsfunktion install_buffer erzeugt das Indent-Buffer-Objekt, fügt es in die Liste des Managers ein und installiert den Buffer.
format::indentbuf * install_buffer(std::ostream & o_s) { format::indentbuf* sbuf(new format::indentbuf(o_s.rdbuf())); buffer_mng::instance()->insert(o_s,sbuf); o_s.rdbuf(sbuf); //out stream buffer pointer setzen return sbuf; }
Die Implementierung des indent-Manipulators war wahrscheinlich aufwendiger, als ursprünglich erwartet, wird aber mit einer faszinierend einfach zu handhabenden Syntax belohnt.
//--------------------------------------------------------------------------------- cout << format::indent(2) << "Line 1 indent = 2\n"; << "Line 2 indent = 2\n" cout << format::indent(4) << "Line 3 indent = 4\n"; cout << format::indent(2) << "Line 3 indent = 2\n"; //---------------------------------------------------------------------------------
Um den Artikel einigermaßen verständlich zu halten, bin ich auf einige Aspekte der Manipulator-Programmierung (Templates, Vererbung, usw.) nicht eingegangen. Wenn Sie tiefer in die Thematik einsteigen möchten, empfehle ich die Artikel von Angelika Langer.
6 Referenzen
- Ein und Ausgabe in C++ - IO-Streams von CStoll
- C++ User Journal - User-Defined Format Flags von Matthew H. Austern
- C++ Artikel von Angelika Langer
-
Gut, nur vielleicht noch etwas mehr auf die Technik eingehen, einige beispielhafte Möglichkeiten mehr (Praxisanwendung) und ich glaube, du meinst allozieren, nicht allokieren
-
Reyx schrieb:
Gut, nur vielleicht noch etwas mehr auf die Technik eingehen, einige beispielhafte Möglichkeiten mehr (Praxisanwendung) und ich glaube, du meinst allozieren, nicht allokieren
Mach ich noch!
-
Hey, das sieht nicht schlecht aus für den Anfang (obwohl mich persönlich mal der Abschnitt 3 interessieren würde). Darf ich mich in meinem Streams-Artikel darauf beziehen?
-
CStoll schrieb:
Hey, das sieht nicht schlecht aus für den Anfang (obwohl mich persönlich mal der Abschnitt 3 interessieren würde). Darf ich mich in meinem Streams-Artikel darauf beziehen?
Klar kannst Du das machen. Ich wollte eigentlich auf deinen Stream-Artikel verweisen, damit ich nicht die ganzen Grundlagen durchkauen muss.
-
Kannst ja trotzdem auf den Stream-Artikel verweisen. Crossrefs sind erlaubt.
-
@Rechtschreibautoren
Bitte durchchecken. Danke
-
Inhalt
- Einleitung
- Manipulatoren ohne Argumente
- Ein Generator für HTML-Manipulatoren ohne Argumente
- Ein Generator für HTML-Manipulatoren mit einem Argument
- Ereignisgesteuerte Manipulatoren
- Referenzen
1 Einleitung
Manchmal benötigen wir für einen benutzerdefinierten Datentypen eine Ein- oder Ausgabefunktionen.
struct name { const char * christian_; const char * family_; };
Um die Struktur auszugeben, würden wir in C wahrscheinlich eine Funktion print_name definieren.
void print_name(name * n) //Ausgabe in C { printf("%s %s\n",n->christian_,n->family_); }
In C++ haben wir noch andere Alternativen. Die C++-IO-Bibliothek ist erweiterbar, da sie auf Überladung, und nicht wie in C auf Formatstrings basiert.
Wir können jedes Objekt obj mit der Anweisungcout << "Objekt = " << obj;
ausgeben. Wir müssen für benutzerdefinierte Datentypen lediglich eine Funktion mit der Signatur
std::ostream& operator<<(std::ostream&, const obj &)
definieren. Ich finde diese Schreibweise sehr angenehm, da immer derselbe Mechanismus verwendet wird. Der Ausgabe-Operator für die Struktur name könnte z.B. folgendermaßen implementiert werden.
std::ostream & operator << (std::ostream & o_s, const name & n) //Ausgabe Operator in C++ { return o_s << std::string(n.christian_) .append(1,' ') .append(n.family_); }
Ein Objekt vom Typ name kann dann nach oben genanntem Muster ausgegeben werden.
int main(int argc, char* argv[]) { name n = {"Bjarne","Stroustrup"}; //----------- Ausgabe in C ----------- print_name(&n); printf("\n"); //----------- Ausgabe in C++ --------- cout << n << endl; }
Manchmal stellt sich allerdings auch noch die Frage, wie ein Objekt dargestellt werden soll.
- Soll zuerst der Nachname und dann der Vorname ausgegeben werden?
- Wird die Zahl hexadezimal, oktal oder dezimal dargestellt?
- Erfolgt die Längenangabe in m, cm oder inch?
- Möchten Sie das Datum "04.07.1966" nicht lieber im Format "4. Juli 1966" darstellen?
- Muss die komplexe Zahl 2 + 3i auch von einem Elektrotechniker verstanden werden? Dann schreiben Sie besser 2 + 3j.
Am besten überlassen wir die Entscheidung demjenigen, der unsere Datentypen verwendet. Um Formate zu steuern, werden meistens Manipulatoren verwendet. Sie vereinfachen die Verwendung der IO-Streams in C++. Um z.B. eine hexadezimale Zahl mit mindestens 4 Stellen auszugeben, die notfalls links mit Nullen aufgefüllt wird schreibt man:
cout << "0x" << std::hex //hexadezimal << std::uppercase //Großbuchstaben << std::setw(4) //Mindestens 4 Zeichen ausgeben << std::setfill('0') //Zeichen mit '0' auffüllen << std::right //rechtsbündig << 123 //Hexadezimal 0x007B << std::setfill(' '); //Zeichen beim nächsten mal wieder mit ' ' auffüllen
C++ bietet die Möglichkeit, diesen Mechanismus für selbstgeschriebene Klassen zu nutzen.
2 Manipulatoren ohne Argumente
Jedes Objekt der Klasse ios_base besitzt ein erweiterbares Array, auf das mit der Memberfunktion iword zugegriffen werden kann. Das Array wird automatisch erweitert, sobald iword mit einem Argument aufgerufen wird, das den bisherigen Wert übersteigt. Wenn ein fester Wert für das Argument von iword verwendet wird, kann die allozierte Variable wie ein benutzerdefiniertes Formatflag verwendet werden. Welchen Wert nehmen wir aber nun für das Argument von iword? Wir müssen irgendwie vermeiden, dass zwei Klassen denselben Speicherplatz für Ihre Formatflags verwenden.
class ios_base { //... inline long& iword(int n); static int xalloc() throw(); //... }
Die Memberfunktion xalloc inkrementiert bei jedem Aufruf den Wert einer internen statischen Integer-Variable. Wir können also den Rückgabewert dieser Funktion dazu verwenden, einen eindeutigen Index für ein Formatflag zu erhalten.
long & my_format_flag(std::ios_base & s) { static int my_index = std::ios_base::xalloc(); return s.iword(my_index); } long get_my_format_flag(std::ios_base & s) { return my_format_flag(s); } void set_my_format_flag(std::ios_base & s, long n) { my_format_flag(s) = n; }
Bem.: Die statische Variable my_index wird einmalig, beim ersten Aufruf von my_format_flag initialisiert.
Um einem benutzerdefinierten Formatflag einen Wert zuzuweisen, kann z.B. eine Enumeration verwendet werden.
struct lenght_s { double millimeter; }; enum my_length_format_flags { length_in_mm = 0, length_in_cm = 1, length_in_dm = 2, length_in_m = 3, length_in_inch = 4, }; static void set_length_format(std::ios_base & s, my_length_format_flags len_fmt) { my_format_flag(s) = len_fmt; }
Ein mit iword alloziertes Element wird mit 0 initialisiert. Deswegen ist length_in_mm der Default-Wert für das benutzerdefinierte Formatflag. Die Definition von Manipulatoren ist jetzt nicht mehr besonders schwierig.
namespace length { std::ostream & mm (std::ostream & os) { set_length_format(os,length_in_mm); return os; } std::ostream & cm (std::ostream & os) { set_length_format(os,length_in_cm); return os; } //... }
Bem.: Die C++-Standard-Bibliothek enthält eine überladene Operatorfunktion, die als Argumente ein ostream-Objekt sowie einen Zeiger auf eine Funktion erwartet.
Jetzt fehlt nur noch die Implementierung des Ausgabeoperators.
std::ostream& operator<<(std::ostream & os, const length_s & length) { std::ostringstream oss; //stringstream if(length_format(os) == length_in_cm) oss << length.millimeter/10.0 << " cm"; else if(length_format(os) == length_in_dm) oss << length.millimeter/100.0 << " dm"; //... else if(length_format(os) == length_in_inch) oss << length.millimeter/25.4 << " inch"; else oss << length.millimeter << " mm"; return os << oss.str(); }
Bem.: Das formatierte Objekt wird zunächst zwischengespeichert und anschließend mit einer Anweisung ausgegeben. Da häufig weitere Manipulatoren auf den Stream einwirken, vermeiden wir dadurch unerwünschte Seiteneffekte.
Nachdem die Operator-Funktion << für die Struktur length überladen wurde, können wir unsere Manipulatoren anwenden:
int main() { length_s len = {12345}; cout << length::mm << len << endl; cout << length::cm << len << endl; }
Die Ausgabe liefert das erwartete Ergebnis.
12345 mm 1234.5 cm
3 Ein Generator für HTML-Manipulatoren ohne Argumente
Als nächstes möchte ich parameterlose Manipulatoren betrachten, die nicht zur Formatierung eines Objekts eingesetzt werden, sondern einen Text in einen Stream einfügen. Solche Manipulatoren werden in der Literatur manchmal auch als Inserter bezeichnet. Betrachten wir einige Inserter die für die Ausgabe von HTML-Dokumenten sinnvoll sind.
//----------------------------------------- std::ostream & table(std::ostream & o_s) { return o_s << "\n<table>" } //----------------------------------------- std::ostream & tr(std::ostream & o_s) { return o_s << "\n<tr>" }
Die Definition solcher Manipulatoren ist Fleißarbeit. Die Arbeit könnte leicht durch einen Generator erledigt werden. In diesem Fall benutze ich den Präprozessor als Generator. Es gibt aber einfach zu viele HTML-Tokens, um alle HTML-Token-Manipulatoren per Hand auszuprogrammieren. Eine alternative Technik wäre z. B die Copy-Paste-Programmierung, deren Vorzüge sicher in jedem guten C++-Buch beschrieben werden. Durch den geschickten Einsatz von Makros können wir mit einer Zeile Code mehrere Manipulatoren definieren. Lassen wir uns also auf ein paar hässliche Makros ein.
Ich habe die Makros in der Headerdatei html_manip_macros.h definiert.
#ifndef __html_manip_macros_21_06_2006_hdr__ #define __html_manip_macros_21_06_2006_hdr__ //----------------------------------------------------------------------------------------------------------------------------------- //Makro zum generieren eines HTML-Manipulator #define MAKE_HTML_MANIPULATOR(name_space,name,start_token,token,end_token) \ namespace { \ namespace html { \ namespace name_space { \ std::ostream & name(std::ostream & o_s) \ { \ return o_s << ##start_token#token##end_token; \ } \ }}} //namespace unbenannt, name_space und html abschließen //----------------------------------------------------------------------------------------------------------------------------------- #define MAKE_HTML_MANIP_SNT(name_space,name,start_token,end_token) MAKE_HTML_MANIPULATOR(name_space,name,start_token,name,end_token) #define MAKE_HTML_MANIP_BEG(name) MAKE_HTML_MANIP_SNT(beg,name,"\n<" ,">") #define MAKE_HTML_MANIP_END(name) MAKE_HTML_MANIP_SNT(end,name,"\n</",">") #define MAKE_HTML_MANIP_BETWEEN(name) MAKE_HTML_MANIPULATOR(between,name,"\n</",name##>\n<##name,">") #define MAKE_HTML_MANIP_OPEN(name) MAKE_HTML_MANIP_SNT(open,name,"\n<"," ") //----------------------------------------------------------------------------------------------------------------------------------- #define MAKE_HTML_MANIP_BEG_END(name) MAKE_HTML_MANIP_BEG(name) MAKE_HTML_MANIP_END(name) #define MAKE_HTML_MANIP_BEG_END_BETWEEN(name) MAKE_HTML_MANIP_BEG_END(name) MAKE_HTML_MANIP_BETWEEN(name) #define MAKE_HTML_MANIP_BEG_END_BETWEEN_OPEN(name) MAKE_HTML_MANIP_BEG_END_BETWEEN(name) MAKE_HTML_MANIP_OPEN(name) #define MAKE_HTML_CLOSE_TOKEN MAKE_HTML_MANIPULATOR(close,token,"",,">") //----------------------------------------------------------------------------------------------------------------------------------- #endif
Bem.: Das Kürzel SNT bedeutet, dass der Name des Manipulators und des Tokens identisch sind (same name as token)
- Der HTML-Manipulator wird in einen unbenannten Namensraum definiert um Konflikte mit Mehrfach-Definitionen zu vermeiden. Alternativ hätten wir den Manipulator auch inlinen können. Dies ist aber nicht sehr sinnvoll, weil beim Aufruf des Manipulators ein Funktionspointer dereferenziert werden muss. Das bedeutet natürlich, dass die Funktion im Objekt-Code vorhanden sein muss. Genau das sollte aber bei der inline-Deklaration vermieden werden, da der Aufruf einer Inline-Funktion durch den Funktionsrumpf ersetzt werden soll.
- Als nächstes wird der Namensraum html im unbenannten Namensraum verschachtelt
- Der nächste Namensraum ist frei wählbar. Sinn und Zweck dieses Namensraums ist es zwischen verschiedenen Arten von Tokens zu unterscheiden (z. B. <table> und </table>).
- Die Präprozessor-Anweisung # wandelt den übergebenen Ausdruck in einen String um.
- Die Angabe von ## ist notwendig um die Ausdrücke verbinden zu können.
- Vorsicht: Nach einer Zeilenumbruchsanweisung ** muss zwingend ein Carrage-Return eingefügt werden (kein Leerzeichen).
Die Vorarbeit war zwar etwas lästig, aber dafür können wir nun in der Datei html_manip.h massenweise Manipulatoren definieren. MAKE_HTML_MANIP_BEG_END_BETWEEN_OPEN generiert je einen HTML-Manipulatoren im Namensraum beg, end, between und open. Die Namensräume sind so gewählt, dass sie ihrer Bedeutung entsprechen.
#ifndef __html_manip_21_06_2006_hdr__ #define __html_manip_21_06_2006_hdr__ #include "html_manip_macros.h" MAKE_HTML_MANIP_BEG_END_BETWEEN_OPEN(table) MAKE_HTML_MANIP_BEG_END_BETWEEN_OPEN(tr) MAKE_HTML_MANIP_BEG_END_BETWEEN_OPEN(td) MAKE_HTML_MANIP_BEG_END_BETWEEN_OPEN(h1) MAKE_HTML_MANIP_BEG_END_BETWEEN_OPEN(h2) MAKE_HTML_CLOSE_TOKEN #endif
#include <iostream> #include "html_manip.h" using namespace std; int main() { cout << html::beg::h1 << " caption 1" << html::end::h1 << html::beg::h2 << " caption 2" << html::end::h2 << html::open::table << "border=\"1\" cellspacing=\"1\" cellpadding=\"1\" align=\"center\"" << html::close::token << html::beg::tr << html::beg::td << " row 1 col 1" << html::between::td << " row 1 col 2" << html::end::td << html::end::tr << html::end::table << endl ; }
Der Code in der main-Funktion ähnelt, in gewisser Weise, der ausgegebenen HTML-Datei.
<h1> caption 1 </h1> <h2> caption 2 </h2> <table border="1" cellspacing="1" cellpadding="1" align="center"> <tr> <td> row 1 col 1 </td> <td> row 1 col 2 </td> </tr> </table>
Die Attribute des HTML-Tokens table habe ich zunächst als String eingefügt. Im nächsten Kapitel möchte ich zeigen, wie man sie in Manipulatoren verpacken kann.
4 Ein Generator für HTML-Manipulatoren mit einem Argument
Beginnen wir mit einem einfachen Beispiel. Das Attribut border soll als Manipulator implementiert werden, dem ein Argument übergeben wird.
Am einfachsten ist es, eine Klasse border zu definieren, die mit Hilfe eines befreundeten Ausgabeoperators in einen Stream geschrieben werden kann.#include <iostream> #include <sstream> using namespace std; //-------------------------------------------------------------------------- class border { friend std::ostream & operator<<(std::ostream &, const border &); private: const unsigned int width_; public: explicit border(unsigned int width) :width_(width) {} //ctor }; //--------------------------------------------------------------------------
Die Klasse besitzt lediglich einen Konstruktor und die friend-Deklaration. Die Ausgabefunktion muss natürlich auch noch geschrieben werden.
//-------------------------------------------------------------------------- std::ostream & operator<<(std::ostream & o_s, const border & b) { ostringstream oss; oss << "border = \"" << b.width_ << "\""; return o_s << oss.str(); } //--------------------------------------------------------------------------
Das wars auch schon. Der Manipulator kann verwendet werden.
//-------------------------------------------------------------------------- int main() { cout << border(1) << endl; } //--------------------------------------------------------------------------
Wir können jetzt die Manipulatoren cellspacing, cellpadding, align, usw. der Reihe nach implementieren.
Da immer das gleiche Implementierungsmuster verwendet wird, bietet es sich an, eine Template-Klasse zu schreiben.template <unsigned long ID,typename Parm1> struct base_attribute { typedef typename base_attribute<ID,Parm1> me; private: Parm1 val_; public: base_attribute(Parm1 val) :val_(val) {} friend std::ostream& operator<<(std::ostream & o_s, const base_attribute<ID,Parm1> & obj); };
Mit einer Typdefinition wird dann der Name für unseren Manipulator erzeugt.
typedef base_attribute<0,unsigned long> border;
Ein Makro erleichtert wieder die Generierung der Manipulatorern.
#define MAKE_HTML_ATTRIB_MANIP(ID,Parm,name,token) \ namespace html { \ namespace attrib { \ typedef base_attribute<ID,Parm> token; \ std::ostream & operator<<(std::ostream & o_s, const base_attribute<ID,Parm> & obj) \ { \ std::ostringstream oss; \ oss << #token << " = \"" << obj.val_ << "\" "; \ return o_s << oss.str(); \ } \ }} #define MAKE_HTML_ATTRIB_MANIPULATOR(Parm,name,token) MAKE_HTML_ATTRIB_MANIP(__LINE__,Parm,name,token) #define MAKE_HTML_ATTRIB_MANIPULATOR_SNT(Parm,name) MAKE_HTML_ATTRIB_MANIPULATOR(Parm,name,name)
Bem.: Die ID ist lediglich ein Dummy-Parameter um beliebig viele Typen generieren zu können. Da der konkrete Wert keine Rolle spielt habe ich das __LINE__-Makro als Übergabe-Parameter verwendet.
Die Manipulatoren für die Ausgabe der HTML-Attribute können nun generiert werden.
MAKE_HTML_ATTRIB_MANIPULATOR_SNT(unsigned long,border) MAKE_HTML_ATTRIB_MANIPULATOR_SNT(unsigned long,cellspacing) MAKE_HTML_ATTRIB_MANIPULATOR_SNT(unsigned long,cellpadding) MAKE_HTML_ATTRIB_MANIPULATOR_SNT(unsigned long,bgcolor) MAKE_HTML_ATTRIB_MANIPULATOR_SNT(const char *,align)
Attributwerte vom Typ *const char ** definiere ich im Namensraum html::attrib::value.
namespace html { namespace attrib{ namespace value { static const char * center = "center"; static const char * justify = "justify"; static const char * left = "left"; static const char * right = "right"; }}}
Die HTML-Attribute für die Tabelle
<table border="1" cellspacing="1" cellpadding="1" align="center">
können jetzt mit Hilfe der Manipulatoren geschrieben werden.
cout << html::open::table << html::attrib::border(1) << html::attrib::cellspacing(1) << html::attrib::cellpadding(1) << html::attrib::align(html::attrib::value::center); << html::close::token << endl;
5 Ereignisgesteuerte Manipulatoren
HTML-Token werden im Allgemeinen, abhängig von der Dokumenten-Hierarchie eingerückt. Es stellt sich die Frage, ob diese Aufgabe von einem Manipulatoren erledigt werden kann. Im Unterschied zu den bisherigen betrachteten Fällen, müssten wir den Stream permanent überwachen. Ich war überrascht, dass ich bei meinen Internet-Recherchen kaum Informationen zu diesem Thema finden konnte. Dabei stellt sich hier eine ganz grundsätzliche Frage. Kann ich einen Manipulator wie z.B. std::hex implementieren, der sich auf alle in den Stream eingefügten Integer-Zahlen auswirkt? Wie wir gleich sehen werden, müssen wir einen Streambuffer erstellen und mit dem Stream verknüpfen. Die Frage könnte also auch anders gestellt werden. Ist es sinnvoll, einem Manipulator die Verantwortung für die Kopplung von Buffer und Stream zu übertragen?
Widmen wir uns aber wieder unserer Aufgabenstellung. Um auf einen Zeilenumbruch reagieren zu können, müssen wir eine von std::basic_streambuf<char_type,traits> abgeleitete Klasse implementieren, die ich im Folgenden als Indent-Buffer-Klasse bezeichne. Da die Basisklasse die virtuelle Methode overflow definiert, die vor jeder Schreiboperation aufgerufen wird, können wir durch Überschreiben der Methode die Ausgabe der Zeichen steuern. Die Hauptaufgabe des Manipulators besteht darin, den Buffer an den übergebenen Stream zu koppeln.Die Indent-Buffer-Klasse besitzt die von std::basic_streambuf übernommenen Template-Parameter, sowie einen zusätzlichen Template-Parameter um das Füllzeichen zu spezifizieren.
#include <streambuf> template <typename char_type, char_type fill_char = ' ', typename traits = std::char_traits<char_type> > struct basic_indent_buf : public std::basic_streambuf<char_type,traits> { //... std::basic_streambuf<char_type,traits> * m_sbuf; int count_; //Anzahl der Füllzeichen bool set_; //Flag ob Füllzeichen bereits eingefügt wurden //... }
Die Membervariable m_sbuf zeigt auf den Buffer des Streams, der mit unserem Manipulator verknüpft ist. Die Einrückungstiefe wird in count_ gespeichert. Außerdem gibt es noch eine Variable, die als Zustandsflag verwendet wird. Die Implementierung von overflow ist nicht besonders schwierig. Sobald ein Zeilenumbruch erkannt wird, wird das Zustandsflag gesetzt. Damit haben wir ein Kriterium, ob bei der nächsten Schreiboperation die Einrückung der Zeile erfolgen muss.
//--------------------------------------------------------------------------------- virtual int_type overflow(int_type c = traits::eof()) //überschreibt std::streambuf::overflow // { if (traits_type::eq_int_type(c, traits_type::eof())) return m_sbuf->sputc(c); if (set_) { //n-mal Füllzeichen einfügen std::fill_n(std::ostreambuf_iterator<char_type>(m_sbuf), count_, fill_char); set_ = false; } if (traits_type::eq_int_type(m_sbuf->sputc(c), traits_type::eof())) return traits_type::eof(); if (traits_type::eq_int_type(c, traits_type::to_char_type('\n'))) //beim Zeilenumbruch wird set_ = true; //das Zustandsflag gesetzt return traits_type::not_eof(c); } //---------------------------------------------------------------------------------
Damit wären die wesentlichen Elemente der Indent-Buffer-Klasse implementiert. Wir können den Indent-Buffer bereits testen.
Um die Ausgabe besser kontrollieren zu können, definieren wir einen Indent-Buffer, der '+'-Zeichen zum Auffüllen verwendet.//... typedef format::basic_indent_buf<char,'+', std::char_traits<char> > plus_indentbuf; //Intend-Buffer mit '+' als Füllzeichen //... int main() { plus_indentbuf sbuf(plus_indentbuf(cout.rdbuf())); //Indent-Buffer Objekt anlegen cout.rdbuf(&sbuf); //Buffer und Stream verknüpfen sbuf.indent(10); //Einrückungstiefe setzen cout << "Die Einrueckungstiefe" << endl << "betraegt" << endl << sbuf.indent() << endl; }
Die Ausgabe zeigt, dass nach jedem Zeilenumbruch eine Einrückung mit 10 '+'-Zeichen erfolgt.
++++++++++Die Einrueckungstiefe ++++++++++betraegt ++++++++++10.
Für den Manipulator benötigen wir noch eine kleine Hilfsstruktur.
struct indent { indent(int i) : indent_(i) {} int indent_; };
Im Funktionsrumpf des indent-Manipulators wird zunächst überprüft, ob bereits ein indentbuf-Objekt mit dem Stream verknüpft ist. Falls nicht wird ein dynamisch erzeugtes Indent-Buffer-Objekt installiert. Die Adresse des Ausgabestreams speichern wir mit Hilfe der Methode pword, die uns Zugriff auf ein erweiterbares Array mit void*-Pointer gewährt.
//--------------------------------------------------------------------------------- std::ostream& operator<< (std::ostream& out, format::indent const& ind) { format::indentbuf* sbuf = dynamic_cast<format::indentbuf*>(out.rdbuf()); //stream pointer lesen if (!sbuf) //wenn streambuffer pointer noch nicht exitstiert { sbuf = new format::indentbuf(out.rdbuf()); //muss er erzeugt werden. out.pword(index) = &out; //pword pointer auf Adresse von out setzen out.register_callback(callback, 0); //callback Funktion registrieren out.rdbuf(sbuf); //out stream buffer pointer setzen } sbuf->indent(ind.indent_); //indent spacing setzen return out; } //---------------------------------------------------------------------------------
Da der Buffer dynamisch alloziert wurde, müssen wir uns darum kümmern, dass der Speicher wieder freigegeben wird. Zu diesem Zweck registrieren wir die Funktion callback. Sie wird aufgerufen sobald das Indent-Buffer-Objekt zerstört wird.
//--------------------------------------------------------------------------------- static int const index = std::ios_base::xalloc(); //--------------------------------------------------------------------------------- void callback(std::ios_base::event ev, std::ios_base& ios, int) { if (ev == std::ios_base::erase_event) { std::ostream* out = static_cast<std::ostream*>(ios.pword(index)); format::indentbuf* sbuf = dynamic_cast<format::indentbuf*>(out->rdbuf()); out->rdbuf(sbuf->sbuf()); delete sbuf; } } //---------------------------------------------------------------------------------
Leider hat die gezeigte Implementierung mindestens zwei schwerwiegende Mängel. Sie treten zu Tage, sobald der Zustand eines Stream mit installiertem Indent-Buffer kopiert wird.
//--------------------------------------------------------------------------------- cerr.copyfmt(cout); //---------------------------------------------------------------------------------
Durch den Aufruf von copyfmt werden die im pword-Array gespeicherten Pointer-Adressen kopiert. Da außerdem alle Callback-Funktionen der Kopiervorlage im Ziel-Stream registriert werden, wird der delete-Operator für jede kopierte Speicheradresse des Buffer aufgerufen, was geradewegs ins Nirwana führt.
Das zweite Problem ist, dass beim Kopieren ein Ressourcen-Leck entsteht, falls im Ziel-Stream bereits ein indent-Buffer installiert ist. Da die Adresse des indent-Buffer-Objekt überschrieben wird kann auf das Objekt nicht mehr zugegriffen werden.
Offensichtlich ist das pword-Array eher dafür ausgelegt, Zeiger auf statische Objekte (z.B. Funktionspointer, etc.) zu speichern. Für das Speichern von dynamisch allozierten Objekten ist das Array auf jeden Fall nicht zu gebrauchen. Es spricht alles dafür, den Manipulator sowie die callback-Funktion einem Redesign zu unterwerfen. Zunächst benötigen wir einen Manager, der die indent-Buffer-Objekte unabhängig von der IO-Library verwalten kann.template <typename char_type, typename traits> class basic_buff_mng { typedef std::ios_base bos; typedef std::basic_streambuf<char_type,traits> bsb; typedef std::map<bos*,bsb*> table_type; typedef typename table_type::value_type value_type; typedef typename table_type::const_iterator const_iterator; static int instances_; table_type list; //... }
Die statische Klassenvariable instances_ speichert, wie viele Instanzen des Buffer-Managers existieren. Der Wert wird im Konstruktor inkrementiert und im Destruktor dekrementiert. Damit wir die Anzahl der Instanzen jederzeit abfragen können, wird eine statische Methode instances zur Verfügung gestellt.
static size_t instances() { return instances_; }
Der Buffer-Manager soll als Singleton implementiert werden, d.h. es darf nur eine einzige Instanz im laufenden Programm geben.
static basic_buff_mng<char_type, traits> * instance() { static basic_buff_mng<char_type, traits> ibm; return &ibm; }
Bei der Instanzzählung interessiert uns lediglich, ob noch eine Instanz existiert. Es kann durchaus passieren, dass die Buffer-Manager-Klasse zerstört wird, obwohl noch Streams wie z.B. std::cout existieren. Das bedeutet, dass ein erase_even event, das am Programmende aufgerufen wird nicht mehr behandelt werden kann, wenn die Instanz des Managers bereits zerstört ist. Aus diesem Grund gibt der Manager beim Aufruf des Destruktors alle verbleibenden Elemente der Liste frei.
~basic_buff_mng() //dtor { --instances_; std::for_each(list.begin(),list.end(),DeleteSecond()); }
Das Funktionsobjekt DeleteSecond ermöglicht die Zusammenarbeit mit der im Header <algorithm> definierten Funktion for_each.
struct DeleteSecond ///< Funktionsobjekt zum Löschen von dynamisch allozierten Objekten { template <typename T,typename U> inline void operator() (std::pair<T,U> & object) const { delete object.second; } };
Beim Aufruf der Methode erase wird ein Element aus der Liste entfernt. Zuvor muss der Speicher für das dynamisch allozierte Stream-Buffer-Objekt freigegeben.
bool erase(std::ios_base & io_s) { delete get(io_s); return(list.erase(&io_s) == 1); }
Das Einfügen von Elementen erfolgt mit der Methode insert. Da der Manager über keine konkreten Typinformationen verfügt, muss sich der Aufrufer um die Erzeugung der Indent-Buffer-Objekte kümmern.
bool insert(bos & o_s, bsb * b_s) { return list.insert(value_type(&o_s, b_s)).second; }
Jetzt kann die callback-Funktion geschrieben werden.
void callback(std::ios_base::event ev, std::ios_base& ios, int x) { if(buffer_mng::instances()) //existiert das Singleton-Objekt noch? { if (ev == std::ios_base::erase_event) { buffer_mng::instance()->erase(ios); } else if(ev == std::ios_base::copyfmt_event) { if(std::ostream & o_s = dynamic_cast<std::ostream &>(ios)) o_s << format::indent(get_indent(ios)); } } }
Die Events werden nur behandelt, wenn die Instanz des Managers noch existiert. Das Löschen des Buffers erledigt der Manager. Beim Kopieren eines Streams wird der Indent-Manipulator aufgerufen, der sich um die Erzeugung der Indent-Buffer-Objekte kümmert. Die Funktion get_indent liefert die in iword gespeicherte Einrückungstiefe des kopierten Streams. Jetzt fehlt nur noch der Ausgabe-Manipulator für die indent-Klasse.
template <typename char_type, typename traits> std::ostream& operator<< (std::basic_ostream<char_type,traits> & o_s, format::indent const& ind) { format::indentbuf* sbuf = dynamic_cast<format::indentbuf*>(o_s.rdbuf()); //stream pointer lesen if (!sbuf) //wenn streambuffer pointer noch nicht exitstiert { std::ios_base & ios(dynamic_cast<std::ios_base &>(o_s)); sbuf = install_buffer(o_s); o_s.register_callback(callback, 0); //callback Funktion registrieren } set_indent(o_s,ind.indent_); sbuf->indent(ind.indent_); //indent spacing setzen return o_s; }
Die Hilfsfunktion install_buffer erzeugt das Indent-Buffer-Objekt, fügt es in die Liste des Managers ein und installiert den Buffer.
format::indentbuf * install_buffer(std::ostream & o_s) { format::indentbuf* sbuf(new format::indentbuf(o_s.rdbuf())); buffer_mng::instance()->insert(o_s,sbuf); o_s.rdbuf(sbuf); //out stream buffer pointer setzen return sbuf; }
Die Implementierung des indent-Manipulators war wahrscheinlich aufwendiger, als ursprünglich erwartet, wird aber mit einer faszinierend einfach zu handhabenden Syntax belohnt.
//--------------------------------------------------------------------------------- cout << format::indent(2) << "Line 1 indent = 2\n"; << "Line 2 indent = 2\n" cout << format::indent(4) << "Line 3 indent = 4\n"; cout << format::indent(2) << "Line 3 indent = 2\n"; //---------------------------------------------------------------------------------
Um den Artikel einigermaßen verständlich zu halten, bin ich auf einige Aspekte der Manipulator-Programmierung (Templates, Vererbung, usw.) nicht eingegangen. Wenn Sie tiefer in die Thematik einsteigen möchten, empfehle ich die Artikel von Angelika Langer.
6 Referenzen
- Ein und Ausgabe in C++ - IO-Streams von CStoll
- C++ User Journal - User-Defined Format Flags von Matthew H. Austern
- C++ Artikel von Angelika Langer
-
Danke für die Korrekturen. Ich habe sie alle übernommen und den Status des Artikels auf E gesetzt.
:xmas1: Übrigens "allokieren" ist gleichbedeutend mit allozieren.
siehe Wikipedia
-
rik schrieb:
Danke für die Korrekturen. Ich habe sie alle übernommen und den Status des Artikels auf E gesetzt.
Also muss ich den ersten Post verschieben?
-
estartu schrieb:
Also muss ich den ersten Post verschieben?
Ja, der erste Post enthält die Korrekturen.
-
Nimm's mir nicht übel, aber an sich sollte der fertige Artikel am Ende dieses Threads plaziert werden, damit wir's leichter zu verschieben haben und nicht jedes mal schauen müssen, welcher Post denn jetzt der aktuellste ist.
-
GPC schrieb:
Nimm's mir nicht übel, aber an sich sollte der fertige Artikel am Ende dieses Threads plaziert werden, damit wir's leichter zu verschieben haben und nicht jedes mal schauen müssen, welcher Post denn jetzt der aktuellste ist.
:xmas2: Ich nehm's Dir nicht übel. Ich habe den Thread jetzt nochmal gepostet. Hoffe so ist es jetzt richtig.
-
rik schrieb:
GPC schrieb:
Nimm's mir nicht übel, aber an sich sollte der fertige Artikel am Ende dieses Threads plaziert werden, damit wir's leichter zu verschieben haben und nicht jedes mal schauen müssen, welcher Post denn jetzt der aktuellste ist.
:xmas2: Ich nehm's Dir nicht übel. Ich habe den Thread jetzt nochmal gepostet. Hoffe so ist es jetzt richtig.
Dankeschön!
Ist auch fast richtig, den Status setze ich jetzt mal beim ersten Post von R auf E, damit man es auch in der Übersicht sieht. :xmas1: