Config-Datei beim Programmstart einlesen
-
Hallo,
ich habe eine Config-Datei, die ich beim Programmstart einlesen will. Die Config-Datei besteht im Moment aus zwei Teilen:
1. Speicherort:Pfad + Verzeichnisname
2. Filter:Start + Ende + ProfitDer Aufbau der Config-Datei ist einfach ein String ohne Leerzeichen, dann ein Tab, dann der dazugehörige Wert:
Pfad C:\dev\ Verzeichnisname Skins Start 110 Ende 230 Profit 13.79Auf die Werte will ich dann folgendermaßen zugreifen:
int main() { cout << "Pfad = " << config::save_as::path << '\n'; cout << "Verzeichnisname = " << config::save_as::directory << '\n'; cout << "Start = " << config::filter::start << '\n'; cout << "Ende = " << config::filter::ende << '\n'; cout << "Profit = " << config::filter::profit << '\n'; }Im Moment mache ich das Ganze so:
#include <fstream> #include <iostream> #include <string> using namespace std; namespace config { template<typename T> T config_value( const string& what ) { for(ifstream file("config.txt");;) { string tag; T value; if( file >> tag >> value ) { if( tag == what ) return value; } else { file.clear(); file.ignore( numeric_limits<streamsize>::max(), '\n' ); } } } namespace save_as { const string path = config_value<string>( "Pfad" ); const string directory = config_value<string>( "Verzeichnisname" ); } namespace filter { const int start = config_value<int>( "Start" ); const int ende = config_value<int>( "Ende" ); const double profit = config_value<double>( "Profit" ); } } int main() { cout << "Pfad = " << config::save_as::path << '\n'; cout << "Verzeichnisname = " << config::save_as::directory << '\n'; cout << "Start = " << config::filter::start << '\n'; cout << "Ende = " << config::filter::ende << '\n'; cout << "Profit = " << config::filter::profit << '\n'; }Zwei Dinge gefallen mir hier aber nicht:
1. Dass ich für jeden Configwert die Config-Datei neu öffne. Ich will sie eigentlich nur einmal öffnen, dabei alle Configwerte holen und sie entsprechend zuweisen.
2. Dass die Funktionconfig_valueöffentlich ist. Ich will, dass diese Funktion nur vonsave_asundfilterverwendet werden kann.Ich habe dafür gerade keinen eleganten Lösungsweg parat. Habt ihr eine Idee? Wie handhabt ihr Config-Dateien? Gibt es hierfür eventuell ein Design Pattern, das ich noch nicht kenne?
Danke im Voraus.
-
Ich hatte neulich ein ähnliches Problem, es ging auch darum Werte bei Programmstart zu setzen, die dann aber nicht mehr verändert werden sollten.
Aufgrund der unspezifiezierten Initialisierungsreihenfolge von globalen Variablen verwende ich RAII-Initialisier/Zerstörer, die sich darum Kümmern, dass diese Variablen initialisiert werden. In der main lege ich dann einfach ein Haufen von denen in der richtigen Reihenfolge an (wenn A B braucht, wird B zuerst erstellt) und die initialisieren mir alles, was ich benötige und zerstören es am Ende auch automatisch. Auch etwaige Singletons werden so initialisiert.
In deinem Fall würd ich das so machen:namespace config { namespace impl { class config_string_holder { public: operator const std::string&() noexcept { return str_; } private: std::string str_; friend initializer; } } config_string_holder foo; config_string_holder bar; class initializer { static bool initalized = false; // <- Pseudocode! public: initializer() { if (initialized) throw too_many_initializers(); initialized = true; // read values foo.str_ = "foo"; bar.str_ = "bar"; } ~initializer() noexcept = default; // cleanup ist vermutlich nicht notwendig }; } int main() { config::intializer config_init; // jetzt kann man auf die werte zugreifen }
-
Ich kann deiner Programmlogik nicht ganz folgen, Nathan. Könntest du das anhand eines detailliertern Beispiels genauer erklären?
-
Ok.
Wenn man globale Variablen verwendet hat man ja immer das Problem der Initialisierungsreihenfolge. Anstatt irgendwelche Tricks mache ich da eine ganz simple Lösung: Ich schreibe mir eine initializer-Klasse für - in meinem Fall - jedes Modul. Der Konstruktor sorgt dafür, dass alle diese globalen Variablen initialisiert werden. Der Destruktor sorgt dafür, dass sie zerstört werden. Die Variablen selber haben keine Konstruktoren/Destruktoren, sondern private create/destroy Funktionen, die dann von der friend-initializer-Klasse aufgerufen werden.
Damit das Modul erzeugt wird, legt man einfach eine initializer-Klasse auf dem Stack in der main an. Hat man nun irgendwelche Abhängigkeiten zwischen Modulen legt man sie eben in der entsprechenden Reihenfolge an, dann kümmert sich der Compiler auch um die richtige Initialisierung und Zerstörung, RAII sei dank. Zugriff auf diese Variablen bevor main aufgerufen wird, wird einfach verboten.
In deinem Fall hat man nun die Config-Werte. Die müssen vom initializer gesetzt werden, sollten aber von keinem anderen geändert werden. Der Typ std::string zum speichern direkt fällt weg und const std::string geht auch nicht, da sie ja erst später initialisiert werden. Deshalb habe ich einen Helfertypen config_string_holder verwendet, der ist implizit in einen std::string konvertierbar, so dass es den meisten Usern nicht auffallen wird, dass es ein anderes Objekt ist*. Den Wert ändern kann aber nur die initializer-Klasse. Man erstellt nun einfach wie gewohnt die globalen Objekte vom Typ config_string_holder und schreibt eine initializer-Klasse. Der Konstruktor setzt einfach die Werte der globalen Variablen entsprechend, indem er die Datei öffnet und sie einliest.
Vor der Verwendung muss nun der initializer erstellt werden, der setzt dann die Werte und dann können sie verwendet werden. Die Datei wird nur einmal geöffnet, du bist evtl. Abhängigkeitsprobleme los und andere Leute können auch nicht auf die Leseinterna zugreifen.*Es sei denn sie rufen Methoden auf, aber die kann man entweder durchreichen oder Vererben. Weiß ja keiner, dass das nicht std::string ist, also wird es auch keiner polymorph nutzen.
-
#include <iostream> #include <string> #include <sstream> #include <map> namespace config { class impl { typedef std::map<std::string, std::string> Values; Values values; public: static impl& get() { static impl instance; return instance; } template<typename T> T read(const std::string& key) { T val; std::stringstream strm; strm << values[key]; strm >> val; return val; } template<typename T> void write(const std::string& key, T val) { std::stringstream strm; strm << val; strm >> values[key]; } void init() { // Variablen laden, die built-in sind } }; } #define ConfigValues config::impl::get() int main() { ConfigValues.init(); // Erstellt das Singleton ConfigValues.write("Dbl_Test", 2.1412); ConfigValues.write("Str_Test", "Muh"); std::cout << ConfigValues.read<double>("Dbl_Test") << "\n"; std::cout << ConfigValues.read<std::string>("Str_Test") << "\n"; }Nicht schön, aber funktionell.
-
out schrieb:
Zwei Dinge gefallen mir hier aber nicht:
1. Dass ich für jeden Configwert die Config-Datei neu öffne. Ich will sie eigentlich nur einmal öffnen, dabei alle Configwerte holen und sie entsprechend zuweisen.Dann öffne die Datei eben nur einmal. Man muss ja keine globalen Variablen benutzen, die sich selbst aus einer Datei laden. Brrr...
out schrieb:
2. Dass die Funktion
config_valueöffentlich ist. Ich will, dass diese Funktion nur vonsave_asundfilterverwendet werden kann.Warum? Und dass deine globalen Variablen überall benutzt werden können, stört dich nicht?
-
Man will doch nicht, dass globale Variablen bei ihrer Definition Dateien öffnen
(bin nicht mal sicher, ob das UB ist, weil die Variablen könnten theoretisch vor std::cout initialisiert werden).Du willst das so haben:
// config.hpp: namespace config { namespace save_as { string const& path(); string const& directory(); } namespace filter { int start(); int ende(); double profit(); } void load(boost::string_ref filename); } // config.cpp: namespace config { namespace { string path_, directory_; int start_; } void load(boost::string_ref filename) { if (ifstream file(filename)) { map<string,string> conf; for (std::string key,val;file>>key>>val;) conf[key] = val; path_ = conf["Pfad"]; directory_ = config["Verzeichnisname"]; start_ = stoi(config["Start"]); ... } } namespace save_as { string const& path(){return path_;} string const& directory(){return directory_;} } namespace filter { int start(){return start_;} int ende(){return ende_;} } }
-
@Nathan: Ok, nun kann ich dir folgen, danke dir. Ich bau meinen Kram mal um und schau, wies mir gefällt.

@asdasdasd: Ja, an sowas hatte ich auch schon gedacht. Dann aber wieder verworfen weil ich den stringstream-Umweg nicht mochte.

TyRoXx schrieb:
out schrieb:
2. Dass die Funktion
config_valueöffentlich ist. Ich will, dass diese Funktion nur vonsave_asundfilterverwendet werden kann.Warum? Und dass deine globalen Variablen überall benutzt werden können, stört dich nicht?
Nein, weil sie genau dafür sind sie vorgesehen. Die Funktion soll dagegen nur an einer bestimmten Stelle benutzt werden.
@proglogik:
proglogik schrieb:
Man will doch nicht, dass globale Variablen bei ihrer Definition Dateien öffnen
(bin nicht mal sicher, ob das UB ist, weil die Variablen könnten theoretisch vor std::cout initialisiert werden).Ich hab mal im Draft nachgeschaut:
n3337/3.6.2/4 schrieb:
It is implementation-defined whether the dynamic initialization of a non-local variable with static storage
duration is done before the first statement of main. If the initialization is deferred to some point in time
after the first statement of main, it shall occur before the first odr-use (3.2) of any function or variable
defined in the same translation unit as the variable to be initialized.Also ist mein Weg nicht gut, weil
implementation-defined. Aber wenn ich das richtige lese, kann ich ja z.B. einfach eine leere Dummy-Funktion in die Übersetzungseinheit tun, und befor ich meine globalen Objekte benutzen will, ruf ich die Dummy-Funktion auf, und dann ist alles sicher initialisiert, oder?