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
-
rik schrieb:
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; } //---------------------------------------------------------------------------------
ich hab eine frage zu dieser Stelle deines Codes, wo definierst du format::indentbuf?
Ach und koenntest du irgendwo den Zusammenhaengenden Source Code uploaden. Den ich hab probleme deine snippets zu kompilieren.
-
OK,den typedef hab ich selber hinbekommen.
typedef basic_indent_buf<char_type, fill_char,traits> indentbuf;
Ich hab aber immer noch probleme deinen Code kompiliert zu bekommen.
plus_indentbuf sbuf(plus_indentbuf(cout.rdbuf())); //Indent-Buffer Objekt anlegen
hier ^ fehlen die konstruktoren und das v
static int const index = std::ios_base::xalloc();
als member oder global space definition geht doch nur in java oder taeusch ich mich da?
-
Sorry für die späte Antwort!
[hier ^ fehlen die konstruktoren und das v
static int const index = std::ios_base::xalloc();
als member oder global space definition geht doch nur in java oder taeusch ich mich da?
std::ios_base::xalloc()
ist eine statische Memberfunktion. Statische Memberfunktionen können aufgerufen werden, ohne das eine Instanz der Klasse vorhanden sein muß. Deswegen kann ich die Variable index damit initialisieren.
Mit welchem Kompiler hast Du den Code kompiliert?
Ich habe den Code mit Visual C++ 8.0 und GCC kompilieren können.
-
ich nutzte den g++ 4.3
-
aber kannst du den ganzen Source Code irgendwo zuganglich machen? Weil ich bei jedem Code Snippet ziemlich am rumfrickeln bin biss der laueft.
-
O.G.Wrapper schrieb:
aber kannst du den ganzen Source Code irgendwo zuganglich machen? Weil ich bei jedem Code Snippet ziemlich am rumfrickeln bin biss der laueft.
Mach ich. Ich habe aber momentan Probleme auf den Server zu kommen.
-
O.G.Wrapper schrieb:
aber kannst du den ganzen Source Code irgendwo zuganglich machen? Weil ich bei jedem Code Snippet ziemlich am rumfrickeln bin biss der laueft.
Die Datei manipulatoren.zip enthält die Beispiele und Testprogramme die ich für den Artikel verwendet habe.
Die Indent-Implementierung habe ich für gcc noch anpassen müssen.
-
Hi,
ich habe deinen sehr interessanten Artikel als Anlass genommen, meine logging Sachen ein wenig zu überarbeiten. Ich denke aber man kommt um die Singleton-Manager Klasser herum. Das Problem ist ja der Fall, dass copyfmt auf einem Indent-Buffer aufgerufen wird. Laut Standard wird aber vor dem copy-Event ein delete-Event auf der zu überschreibenden Instanz aufgerufen. An dieser Stelle kann man in Ruhe den streambuf wiederherstellen und pword(i) löschen. Bei dem direkt folgenden copy-Event kann man dann einen neuen streambuf erstellen und die wichtigen Attribute des alten kopieren.Hier ein (noch nicht ganz vollständiger) Vorschlag:
namespace scm { namespace logging { class log_streambuf_base { public: typedef shared_ptr<log_streambuf_base> log_streambuf_pointer; public: log_streambuf_base() {}; virtual ~log_streambuf_base() {}; //virtual log_streambuf_pointer clone() const = 0; }; // class log_streambuf_base template <typename char_type, typename traits = std::char_traits<char_type> > struct basic_log_streambuf : public log_streambuf_base, public std::basic_streambuf<char_type,traits> { public: typedef typename traits::int_type int_type; typedef typename traits::pos_type pos_type; typedef typename traits::off_type off_type; private: typedef std::basic_streambuf<char_type, traits> streambuf_type; public: basic_log_streambuf(streambuf_type* sbuf) : _streambuf(sbuf), _fill_char(' '), _indent_count(0), _indent_width(4), _set_indent(true) { } virtual ~basic_log_streambuf() { sync(); } //log_streambuf_pointer clone() const { // return make_shared<basic_log_streambuf<char_type, traits> >(*this); //} void copy_indention_attributes(const basic_log_streambuf<char_type, traits>& rhs) { _fill_char = rhs._fill_char; _indent_count = rhs._indent_count; _indent_width = rhs._indent_width; _set_indent = rhs._set_indent; } char_type fill_char() const { return (_fill_char); } void fill_char(char_type c) { _fill_char = c; } int indention() const { return _indent_count; } void indention(int i) { _indent_count = i; } int indention_width() const { return _indent_width; } void indention_width(int i) { _indent_width = i; } streambuf_type* original_rdbuf() { return (_streambuf); } private: basic_log_streambuf(const basic_log_streambuf<char_type, traits>& rhs) : _streambuf(rhs._streambuf), _fill_char(rhs._fill_char), _indent_count(rhs._indent_count), _indent_width(rhs._indent_width), _set_indent(rhs._set_indent) { } // declared, never defined basic_log_streambuf<char_type, traits>& operator=(const basic_log_streambuf<char_type, traits>& ); // override std::streambuf::overflow virtual int_type overflow(int_type c = traits::eof()) { if (traits::eq_int_type(c, traits::eof())) { return _streambuf->sputc(static_cast<char_type>(c)); } if (_set_indent) { std::fill_n(std::ostreambuf_iterator<char_type>(_streambuf), _indent_count * _indent_width, _fill_char); _set_indent = false; } if (traits::eq_int_type(_streambuf->sputc(static_cast<char_type>(c)), traits::eof())) { return traits::eof(); } if (traits::eq_int_type(c, traits::to_char_type('\n'))) { _set_indent = true; } return traits::not_eof(c); } private: streambuf_type* _streambuf; char_type _fill_char; int _indent_count; int _indent_width; bool _set_indent; // log_level // logger // -> sync }; // class basic_log_streambuf typedef basic_log_streambuf<char> log_streambuf; typedef basic_log_streambuf<wchar_t> wlog_streambuf; } // namespace log } // namespace scm
namespace scm { namespace logging { namespace detail { __scm_export(core) int log_streambuf_index(); } // namespace detail class indent { public: indent(int i) : _indention(i > 0 ? i : 0) {} template <typename char_type, typename traits> std::basic_ostream<char_type, traits> &operator()(std::basic_ostream<char_type, traits>& os) const { typedef basic_log_streambuf<char_type, traits> log_streambuf_type; log_streambuf_type* lsb = dynamic_cast<log_streambuf_type*>(os.rdbuf()); if (!lsb) { lsb = install_log_streambuf(os, detail::log_streambuf_index()); os.register_callback(callback, detail::log_streambuf_index()); } lsb->indention(_indention); return (os); } private: template <typename char_type, typename traits> static basic_log_streambuf<char_type, traits>* install_log_streambuf(std::basic_ostream<char_type, traits>& os, int index) { typedef basic_log_streambuf<char_type, traits> log_streambuf_type; log_streambuf_type* log_rdbuf = new log_streambuf_type(os.rdbuf()); os.rdbuf(log_rdbuf); os.pword(index) = log_rdbuf; assert(log_rdbuf == os.rdbuf()); assert(log_rdbuf == os.pword(index)); //assert(os.pword(index) == os.rdbuf()); return (log_rdbuf); } template <typename char_type, typename traits> static void uninstall_log_streambuf(std::basic_ostream<char_type, traits>& os, int index) { typedef basic_log_streambuf<char_type, traits> log_streambuf_type; log_streambuf_type* old_ptr = static_cast<log_streambuf_type*>(os.pword(index)); log_streambuf_type* old_ptr_rd = dynamic_cast<log_streambuf_type*>(os.rdbuf()); assert(0 != old_ptr); assert(old_ptr == old_ptr_rd); assert(0 != os.pword(index)); //assert(os.pword(index) == os.rdbuf()); log_streambuf_type* log_rdbuf = static_cast<log_streambuf_type*>(os.pword(index)); os.rdbuf(log_rdbuf->original_rdbuf()); delete log_rdbuf; os.pword(index) = 0; } static void callback(std::ios_base::event ev, std::ios_base& ios_obj, int index) { if (ev == std::ios_base::erase_event) { // ok if we are here this means, we are a log_streambuf about to be deleted // this could mean, we get simply deleted or we are about to get copied assert(0 != ios_obj.pword(index)); if (std::ostream& os = dynamic_cast<std::ostream&>(ios_obj)) { uninstall_log_streambuf(os, index); } else if (std::wostream& os = dynamic_cast<std::wostream&>(ios_obj)) { uninstall_log_streambuf(os, index); } else { throw std::runtime_error("scm::log::indent::callback() runtime error, failed to dynamic_cast ios_obj."); } } else if(ev == std::ios_base::copyfmt_event) { // ok here we should have gone though the erase_event and now our pword // has the new log_streambuf pointer assert(0 != ios_obj.pword(index)); if (std::ostream& os = dynamic_cast<std::ostream&>(ios_obj)) { log_streambuf* copied_log_buf = static_cast<log_streambuf*>(os.pword(index)); log_streambuf* new_log_rdbuf = install_log_streambuf(os, index); new_log_rdbuf->copy_indention_attributes(*copied_log_buf); } else if (std::wostream& os = dynamic_cast<std::wostream&>(ios_obj)) { wlog_streambuf* copied_log_buf = static_cast<wlog_streambuf*>(os.pword(index)); wlog_streambuf* new_log_rdbuf = install_log_streambuf(os, index); new_log_rdbuf->copy_indention_attributes(*copied_log_buf); } else { throw std::runtime_error("scm::log::indent::callback() runtime error, failed to dynamic_cast ios_obj."); } } } private: int _indention; }; template <typename char_type, typename traits> inline std::basic_ostream<char_type, traits>& operator<<(std::basic_ostream<char_type,traits>& os, const scm::logging::indent& indention) { return (indention(os)); } } // namespace log } // namespace scm
Was meinst du dazu?
P.S. Das seltsamste was mir seit langem untergekommen ist: Ich gehe das Ganze im Debugger Schritt für Schritt durch und in der install_log_streambuf Methode erhalte ich einen Fehler beider letzten Assertion:
log_streambuf_type* log_rdbuf = new log_streambuf_type(os.rdbuf()); os.rdbuf(log_rdbuf); os.pword(index) = log_rdbuf; assert(log_rdbuf == os.rdbuf()); assert(log_rdbuf == os.pword(index)); assert(os.pword(index) == os.rdbuf());
Das Ganze unter Visual Studio 9.0 SP1 x64. Der Pointer, welcher an os.rdbuf übergeben wird ist in der Funktion komischerweise um 8 erhöht, wenn man das Ergebniss von os.rdbuf aber wieder in eine Variable speichert stimmt alles wieder, jedoch beim direkten Vergleich geht etwas gewaltig vor den Baum.
Beste Grüße
-chris
-
Chris Lux schrieb:
Hi,
Ich denke aber man kommt um die Singleton-Manager Klasser herum. Das Problem ist ja der Fall, dass copyfmt auf einem Indent-Buffer aufgerufen wird.
...
-chrisHallo Chris. Wenn man die Manager Klasse vermeiden kann wäre das auf jeden Fall ein Performancevorteil. Ich muss mir Deine Implementierung in Ruhe anschauen um Deine anderen Frage zu beantworten. Es ist halt schon eine Weile her das ich den Artikel geschrieben habe.
-
Chris Lux schrieb:
Hi,
Laut Standard wird aber vor dem copy-Event ein delete-Event auf der zu überschreibenden Instanz aufgerufen.Ich verwende hauptsächlich den Visual Studio 2005 Compiler, der sich diesbezüglich leider nicht ganz so standardkonform verhält.
(delete-Event wird vor dem Kopieren nicht aufgerufen)Chris Lux schrieb:
static void callback(std::ios_base::event ev, std::ios_base& ios_obj, int index)
{
...if (std::ostream& os = dynamic_caststd::ostream&(ios_obj)) {
uninstall_log_streambuf(os, index);
}
else if (std::wostream& os = dynamic_caststd::wostream&(ios_obj)) {
uninstall_log_streambuf(os, index);
}
else
{
throw std::runtime_error("scm::log::indent::callback() runtime error, failed to dynamic_cast ios_obj.");
}
...
}Wenn ein dynamic cast für ein Referenzobjekt schief geht wird eine std::bad_cast exception ausgelöst.
Dagegen liefert der Aufruf eines dynamic cast für einen Pointers NULL wenn die Konvertierung nicht möglich ist.
Der Test für das std::wostream& Objekt wird also nie erreicht. Für eine ernsthafte Implementierung würde ich die
Callback-Funktion als template-funktion registrieren. Die Fallunterscheidung fällt dann natürlich weg.template <typename char_type, typename traits> inline void callback(std::ios_base::event ev, std::ios_base& ios, int index) { //... typedef std::basic_ostream<char_type,traits> basic_ostream; if(basic_ostream * os = dynamic_cast<basic_ostream *>(&ios_obj)) { uninstall_log_streambuf(*os, index); } //... }
Bei mir wurde übrigens eine bad_cast Exeption beim Lösch-Event ausgelöst.
Ich habe inzwischen noch eine alternative Implementierung die auch funktionieren müsste.
Da der cast beim Löschen schief ging, habe ich den ostream einfach in pword gespeichert.
In der ersten if-Abfrage prüfe ich ob schon ein indent buffer Objekt existiert. Wenn ja
wird natürlich kein neuer Speicher reserviert und ein Null-Pointer zurückgegeben.template <typename char_type, typename traits> inline format::indentbuf* install_buffer(std::basic_ostream<char_type, traits> & o_s) { if(format::indentbuf * test = dynamic_cast<format::indentbuf *>(o_s.rdbuf())) { return NULL; } format::indentbuf* sbuf(new format::indentbuf(o_s.rdbuf())); o_s.rdbuf(sbuf); o_s.pword(indent_index) = &o_s; return sbuf; }
Die uninstall Funktion überprüft das Objekt vor dem Löschen.
template <typename char_type, typename traits> inline void uninstall_buffer(std::basic_ostream<char_type, traits> & o_s) { if(format::indentbuf * sbuf = dynamic_cast<format::indentbuf *>(o_s.rdbuf())) { delete sbuf; o_s.pword(indent_index) = 0; o_s.rdbuf(0); } }
Das ist sinnvoll weil der Übergabeparameter per static_cast nicht typsicher konvertiert werden kann.
Beim Copy-Event wird nur bei Bedarf Speicher alloziert.template <typename char_type, typename traits> inline void callback(std::ios_base::event ev, std::ios_base& ios, int index) { typedef std::basic_ostream<char_type,traits> basic_ostream; if (ev == std::ios_base::erase_event) { basic_ostream * bos = static_cast<basic_ostream *>(static_cast<basic_ostream *>(ios.pword(index))); if(bos) { uninstall_buffer<char_type,traits>(*bos); } } else if(ev == std::ios_base::copyfmt_event) { if(std::ostream * o_s = dynamic_cast<std::ostream *>(&ios)) // avoid bad cast exception { *o_s << format::indent(get_indent(ios)); } } }
Den inserter habe ich jetzt wie folgt definiert
template <typename char_type, typename traits> inline 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 { sbuf = install_buffer(o_s); if(sbuf) { o_s.register_callback(callback<char_type,traits>, indent_index); } } set_indent(o_s,ind.indent_); sbuf->indent(ind.indent_); //indent spacing setzen return o_s; }
Chris Lux schrieb:
Was meinst du dazu?
P.S. Das seltsamste was mir seit langem untergekommen ist: Ich gehe das Ganze im Debugger Schritt für Schritt durch und in der install_log_streambuf Methode erhalte ich einen Fehler beider letzten Assertion:log_streambuf_type* log_rdbuf = new log_streambuf_type(os.rdbuf()); os.rdbuf(log_rdbuf); os.pword(index) = log_rdbuf; assert(log_rdbuf == os.rdbuf()); assert(log_rdbuf == os.pword(index)); assert(os.pword(index) == os.rdbuf());
Wenn A == C und B == C, dann ist hier A != B. Das ist natürlich nicht ganz so intuitiv.
rdbuf gibt ein ios_base Objekt zurück, das nach void* konvertiert wird.
Die ios_base Klasse hat den void * Konvertierungs-Operator implementiert.operator void *() const; { // assert(sizeof(this) == sizeof(void*)); // ??? }
Mit besten Grüßen
Erik