Design-Frage: Config-File-Klasse



  • Hi,

    ich bin dabei eine Config-File-Klasse zu schreiben. Ich bin was Desing angeht nicht so fit und poste das Ganze einfach mal hier:

    #ifndef CONFIG_H
    #define CONFIG_H
    
    #include <map>
    #include <string>
    
    class Config
    {
    private:
      std::string filename;
      void readFile(std::string const&); // reads in the file 'filename'
      std::string& trim(std::string&);   // removing whitespace
    
    protected:
      std::map<std::string,std::string> variables;  // contains variables name and values
    
    public:
      Config(std::string const&);
    
      void writeToFile(std::string const&, std::string const&);
    
      // Overloaded operator () - Example:
      //   Config config(myConfigFile);
      //   cout << "Some_Variable=" << config("Some_Variable") << endl;
      std::string const& operator()(std::string const&);
    
      friend std::ostream& operator << (std::ostream& os, const Config& config);
    };
    
    std::ostream& operator << (std::ostream& os, const Config& config);
    
    #endif
    

    Die Dateien haben momentan folgende Form:

    # ***************************************************
    # *** Configuration file ...
    # ***************************************************

    # ein Deluxe-Kommentar: wird direkt ignoriert
    Variable = /home/karl/Dev/irgendwas

    eine_andere_Variable = TRUE

    Bisher werden alle Parameter als std::strings gelesen und geschrieben.
    Ist natürlich nicht so spitze. Schöner wäre, wenn man auch bool int und double schreiben bzw. könnte.

    Jetzt könnte man ein getInt(std::string variable) und setInt(std::string variable, int value) und entsprechende Funktionen für alle Typen machen. Ist sowas ok?
    Oder wie könnte man das noch machen? Vielleicht irgendwie mit Templates?

    Ich bitte um konstruktive Kritik (aber nicht zu hart 😉 )

    Gruss, KB



  • Karl Blau schrieb:

    Ich bitte um konstruktive Kritik (aber nicht zu hart 😉 )

    Hallo Karl,

    so rein optisch würde ich es begrüßen, wenn die public-Member oben in der Klasse beschrieben sind. Das ist schließlich das was den Kunden - also den, der nur die public's nutzen darf - interessiert.

    Bei der Definition der Methoden, bietet es sich an, aussagekräftige Variablennamen zu verwenden; statt gar keine. Also z.B.:

    std::string const& operator()( std::string const& key ); // key; also der Schlüssel für den gewünschten Wert
    

    Ich halte es für einen Vorteil, Member-Variablen immer besonders zu kennzeichnen. Entweder mit vorangestellten 'm_' oder mit angehängtem '_'; ist beides üblich. Das ist positiv für die Lesbarkeit des Codes.

    Dein Konstruktor ist implizit. Das könnte gefährlich sein. Besser das Key-Wort explicit verwenden.

    public:
      explicit Config( std::string const& filename );
    

    Die Methode write_to_file und der operator<< scheint mir irgendwie doppelt. Im allgemeinen bietet es sich an, immer std::istream und std::ostream als IO-Schnittstelle zu definieren. Das bringt mehr Flexibilität.

    Karl Blau schrieb:

    Jetzt könnte man ein getInt(std::string variable) und setInt(std::string variable, int value) und entsprechende Funktionen für alle Typen machen. Ist sowas ok?
    Oder wie könnte man das noch machen? Vielleicht irgendwie mit Templates?

    gibt es. Im Prinzip geht das so:

    class Config
    {
    public:
        explicit Config( std::string const& filename );
    
        template< typename T >
        T get( std::string const& key )
        {
            std::stringstream buf( variables[key] );
            T x;
            if( !(buf >> x) )
                throw std::runtime_error( "kann Variable nicht lesen" );
            return x;
        }
        // ...
    

    und der Aufruf ist dann:

    Config cfg("config.ini");
        int i = cfg.get< int >( "eine_int_Variable" );
    

    Gruß
    Werner



  • Vielen Dank für die Hinweise.

    Doku habe ich gemacht.

    Wegen write_to_file und der operator<<:
    write_to_file schreibt den Übergebenen Wert in die Datei, d.h. eine Variable wird neu belegt. operator<< wurde für die Ausgabe am Bildschirm überladen: eine übersichtliche tabellarische Übersicht über die in der Datei enthaltenen Variablen.
    Scheinbar ist die Namensgebung aber nicht so gelungen - sonst wäre die Frage erst garnicht aufgetreten.

    Schönen Dank!

    Ich versuche mich noch an einer Set-Funktion. Ich poste hier irgendwann mal was ich gemacht habe...

    Gruss, KB



  • Karl Blau schrieb:

    ich bin dabei eine Config-File-Klasse zu schreiben.

    Hehe, da arbeiten wir wohl momentan an der gleichen Sache. 🙂

    Karl Blau schrieb:

    Bisher werden alle Parameter als std::strings gelesen und geschrieben.
    Ist natürlich nicht so spitze. Schöner wäre, wenn man auch bool int und double schreiben bzw. könnte.

    Jetzt könnte man ein getInt(std::string variable) und setInt(std::string variable, int value) und entsprechende Funktionen für alle Typen machen. Ist sowas ok?
    Oder wie könnte man das noch machen? Vielleicht irgendwie mit Templates?

    Ja, ist durchaus ok. Zumindest werde ich es in ähnlicher Form für alle meine Typen (boolean, integer, real, (character), string) machen. Die gepostete Version von Werner mit den Stringstreams ist zwar einfach, mir aber zu unflexibel. ZB bekommt man schon hier Probleme damit:

    fullscreen = false
    

    Evtl. werde ich hier auf ein entsprechendes Design Pattern, zB Strategy, zurückgreifen, damit die einzelnen Funktionen eine Ebene runter rutschen und für den Anwender unsichtbar bleiben. Darüber muss ich mir aber erst noch ein paar Gedanken machen, inwiefern das machbar und sinnvoll ist.



  • groovemaster schrieb:

    Die gepostete Version von Werner mit den Stringstreams ist zwar einfach, mir aber zu unflexibel. ZB bekommt man schon hier Probleme damit:

    fullscreen = false
    

    keineswegs 🕶

    Wenn bool's immer als true oder false angegeben werden, so füge einfach den Manipulator boolalpha hinzu

    T get( std::string const& key )
        {
            std::stringstream buf( variables[key] );
            T x;
            if( !(buf >> std::boolalpha >> x) ) ..
    

    Anwendung wie gehabt:

    bool fullscreen = cfg.get< bool >( "fullscreen" );
    

    .. und wenn das auch nicht reicht, so schreibst Du Dir Deinen eigenen Typ

    struct MySuperBool
    {
        bool as_bool() const { return m_flag; }
        bool m_flag;
    };
    std::istream& operator>>( std::istream& in, MySuperBool& b )
    {
         // hier kannst Du jetzt aus 'in' einlesen, was immer Du magst
         b.m_flag = ..;
         return in;
    }
    

    Und die Anwendung:

    bool fullscreen = cfg.get< MySuperBool>( "fullscreen" ).as_bool();
    

    wieviel Flexibilität willst Du noch?

    Gruß
    Werner



  • Werner Salomon schrieb:

    wieviel Flexibilität willst Du noch?

    Viel mehr. 😉

    ZB wird es bei mir Ganzzahlen, basierend auf einem variablen Zahlensystem, geben.

    flags = 19 # wird ganz normal als dezimaler Wert interpretiert
    flags = 2x10011 # wird als Binärzahl interpretiert

    Und was ist zB mit unquote? Strings werden bei mir wie folgt angegeben:

    user = "Al Bundy"

    Zudem soll es später auch einfache Verknüpfungen geben:

    user_1 = "Al Bundy"
    user_2 = "Peggy Bundy"
    group = user_1 & ";" & user_2

    Für all die Sachen muss ich schwerere Geschütze als Stringstreams auspacken. Für meinen Fall sind sie jedenfalls relativ ungeeignet. Wenn überhaupt, dann nur für spezielle Teile.



  • groovemaster schrieb:

    Für all die Sachen muss ich schwerere Geschütze als Stringstreams auspacken. Für meinen Fall sind sie jedenfalls relativ ungeeignet. Wenn überhaupt, dann nur für spezielle Teile.

    Ehrlich gesagt, verstehe ich Dich nicht. Ich kann Dir für jedes Deiner Beispiele eine Implementierung hinschreiben.
    Du kannst auch innerhalb eines selbstgeschriebenen Streaming-Operators mit Sachen wie regex oder Spirit auf den Input losgehen und ein Objekt von der Klasse Deiner Wahl zurückgeben.
    Deine Schnittstelle ist auch kein stringstream sonern ein istream; und ein ifsream ist auch nur ein istream. Also was bitteschön willst Du machen, was nicht innerhalb eines Streaming-Operators zu lösen wäre?

    Gruß
    Werner



  • Frage für "get":

    val soll in temp geschrieben werden:

    std::string temp;
    std::stringstream buf(temp);
    if (!(buf << val))
      std::cout << "aus val=" << val << " mach " << temp << std::endl;
    

    Funktioniert so nicht. Es kommt nichts in temp an...

    Mache ich was falsch (bestimmt ...)

    Gruss, KB



  • Karl Blau schrieb:

    Frage für "get":
    val soll in temp geschrieben werden:

    .. dann meins Du set - oder?

    Karl Blau schrieb:

    std::string temp;
    std::stringstream buf(temp);
    if (!(buf << val))
      std::cout << "aus val=" << val << " mach " << temp << std::endl;
    

    Funktioniert so nicht. Es kommt nichts in temp an...

    Mache ich was falsch (bestimmt ...)

    Äh! - ja.
    Besser:

    std::stringstream buf;  // leerer Stringstream
    if ( buf << val )       // hinein mit 'val'
    {
        // ja, es ist gutgegangen
        std::string temp = buf.str();   // Inhalt in Form eines 'std::string' wieder rausholen
        std::cout << "aus val=" << val << " mach " << temp << std::endl; // und dem werten User sichtbar machen
    

    Gruß
    Werner



  • Werner Salomon schrieb:

    Ehrlich gesagt, verstehe ich Dich nicht. Ich kann Dir für jedes Deiner Beispiele eine Implementierung hinschreiben.

    Ja und jedesmal wie du das mit MySuperBool gemacht hast, richtig? Dann sind Stringstreams aber keine Universallösung mehr. Ob du dann für jeden Typ eine eigene Klasse machst, eigene Funktionen, überladene Operatoren oder was auch immer, ist ja erst mal nebensächlich. Du musst jedenfalls jeden Typ gesondert behandeln. Und darauf bezog sich wohl Karls Frage, wenn ich ihn richtig verstanden habe.

    Werner Salomon schrieb:

    Du kannst auch innerhalb eines selbstgeschriebenen Streaming-Operators mit Sachen wie regex oder Spirit auf den Input losgehen und ein Objekt von der Klasse Deiner Wahl zurückgeben.

    Regex? Spirit? Boost? Ne, so schwere Geschütze müssen es dann auch wieder nicht sein. 😉


Anmelden zum Antworten