Managing your application's settings can be a big headache. John Torjo's Straightforward Settings Library can help simplify the complexity.
September 25, 2007
URL:http://www.drdobbs.com/cpp/straightforward-settings/202101554
John Torjo is the General Manager of Macadamian Romania. Even after 10+ years, he's still in love with C++. He can be reached at [email protected].
Storage of settings is an issue you've no doubt dealt with many times. You've seen settings stored in .ini files, or in the registry. What you need is a simple library to abstract this away—and that's what I'm about to present. So leave the coffee behind, and get ready for less stress!
Settings are key to flexibility—your application will adapt to the user's needs. This is good for your customer, for your boss, and for you.
However, different settings might be kept in different places. But to you, the programmer, this can be made transparent. What I'll present here is code that will do just that. At the beginning of the application, you'll specify the locations for keeping the settings with just a few lines of code. If some of your settings' locations need to be changed, you'll only need to change one or two lines of code. How's that for cool?
So how do you use it? Well, it's called straightforward for a reason:
// get type val = setting(name); // set setting(name) = val; // forced get type val = setting<type>(name); // forced set setting<type>(name) = val;
Listing One shows a few examples.
// get std::string name = setting("user.name"); std::string pass = setting("user.passw"); long retries = setting("app.retries"); // set setting("app.retries") = 5; setting("user.name") = "John"; setting("user.passw") = "secret"; // forced get long w = setting<long>("width"); // forced set setting<unsigned long>("width") = 12400;
You'll need the forced get and forced set in more complex scenarios, when implicit conversions might come into place, when you use the constant in a templated function, when you want to force a constant to be of a certain type, and so on; see Listing Two.
// this needs a forced get // otherwise, it wouldn't know which operator* to call int size = setting<int>("width") * setting<int>("height"); struct employee { operator std::string() const { return name; } ... }; employee e; ... // assuming employee does not have an operator<<, // this needs a forced set (which will cause the automatic // conversion of e to std::string) setting<std::string>("user.name") = e;
Setting names are case-insensitive. The last thing you'll need is to think: Is it "user.name", "User.name", "User.Name", or "USER.NAME"? No matter what convention you use, the unfortunate truth is that someone will break it. You don't want that; check out Listing Three.
// the following are equivalent std::string s = setting("user.name"); std::string s = setting("user.Name"); std::string s = setting("User.name"); std::string s = setting("uSer.naMe"); // the following are equivalent setting("user.name") = s; setting("User.name") = s; setting("USER.NAME") = s;
When settings are this easy, you can get carried away. In fact, a lot of the application constants can become runtime constants (that is, be read as settings). This brings flexibility up a gear—which is great! Imagine doing profiling—you go through the .ini file, change a few values, see how the application behaves. Tuning your app will become a lot less painful.
One thing to remember is that when using a runtime constant, use the const_ function, instead of setting. You'll have a few advantages:
Note that you can use forced get when using const_. Listing Four shows you how to use the constants.
int r = const_("user.retries"); std::string welcome = const_("app.welcome_msg"); int max_users = const_("app.max_users"); int def_size = const_<int>("w.def_width") * const_<int>("w.def_height");
The setting names contain dots in them. This is intentional. First of all, it delimits scope of the setting: "chart.window.width" tells you that it's a setting related to the "chart" module, it's about its window, and it's about the window's width.
More than that, the dots can specify the actual storage destination: be it a file, a read-only file, the registry, some web site, etc. Based on the name of the setting (as you'll see below), the library will select where it reads or writes the setting.
The predefined storage classes are file_storage and registry_storage, but you can extend the library, by adding your own storage classes.
Note that for a setting, its type must be IOstream friendly (have operator<< defined, in order to be read, and operator>> defined, in order to be written). As a bonus, I allow for automatic conversions between strings of different types (from std::string to std::wstring, and vice versa).
The storage class will always deal with the setting values as strings. However, for each setting it deals with, it will also know its original type, thus, it can optimize the storage. For instance, the registry storage class will keep integers as DWORDs in the registry. See Listing 5.
struct setting_storage { ... virtual void get_setting( const std::string &name, std::string &val, const std::type_info&) const = 0; virtual void set_setting( const std::string &name, const std::string &val, const std::type_info& ) = 0; ... }; struct registry_setting_storage : setting_storage { ... virtual void set_setting( const std::string &name, const std::string &val, const std::type_info& t) { if ( t == typeid(int) || t == typeid(long) || t == typeid(short) || ...) write_to_registry_as_DWORD(name, val); else write_to_registry_as_SZ_string(name,val); } };
As a side note, the file_storage class has an option to save the settings at a certain interval (on a dedicated thread). file_storage also allows you to have comments, as shown in Listing 6.
# when connecting to server, # and it fails, how many times should we try again? app.retry_count=10 # when delete dummy temporary files, at what period should we do it? app.del_temp_files_ms=5000
This is the part where you set where each of your settings are kept—the storages, that is. It's quite simple: you define the function ss::init_settings, and in its body, add storages. For each added storage, you specify the name_root, and the type of storage.
Any name that starts with name_root (it's of type "name_root.subname") belongs to the given storage. See Listing Seven.
void ss::init_settings() { def_cfg().add_storage("", file_storage("app.ini"); def_cfg().add_storage("user", registry_storage("CU/Software/MyApp/user"); def_cfg().add_storage("chart", registry_storage("CU/Software/MyApp/chart"); } // Some settings: // app.retries - from app.ini file // user.name - from registry // user.passw - from registy // chart.buffer_size - from registry // chart.window.width - from registry // chart.window.height - from registry // tmp.del_period - from app.ini file
Once you've defined ss::init_settings, the library will do its thing. You'll love the fact that configuring the storages is just 2-3 lines of code. Even more, you can have multiple configurations—on each configuration, read the settings from somewhere else. For instance, on debug mode, read the settings from an .ini file, in release mode, read the settings from the registry. This is a great aid when debugging— it's much simpler to edit a file than the registry. Besides, it's much easier to test multiple configurations. Just have different versions of the .ini file (or, select the name of the configuration file through a command line argument. Why not?). Just think how complicated that would be if you were to deal with the registry directly!
As said, you define ss::init_settings, and specify where the settings are kept. Here's my favorite: if the current directory (or some specific directory) contains a somefilename.ini file, read the settings from there. Otherwise, read the settings from the registry. Usually, for me, this means: in debug mode, I deal with an .ini file—easily modifiable. As a matter of fact, it's part of the (VC) project's files. In release, I deal with the registry. However, if I run into problems in release mode, I can easily duplicate the debug configuration (simply copy the .ini file where I need it) and vice-versa (just remove or rename the .ini file).
Ok, so what happens when I want a setting, but it's nowhere to be found? Most of the time, you'll be happy with the default constructed value, which is 0, false, or yourclass().
In case you aren't, you can also set defaults in ss::init_settings. You can set a default just like you would a normal setting, or you can do bulk defaults; See Listing Eight.
void ss::init_settings() { def_cfg().add_storage("", file_storage("app.ini"); ... // set defaults setting("user.name") = "John"; setting("user.passw") = "secret"; setting("app.retries") = 5; // bulk set of defaults (up to 40 arguments) bulk_setting( "chart.window.width=100", "chart.window.height=50", "tmp.del_period=20"); }
Note that some existing APIs allow you to specify an extra argument, which is to be returned in case the setting is not found. In practice, this just doesn't pay off. You'll forget it some times (in case you need to use the same setting in several places), or even worse, use different defaults in different places. Bugs like that come back to haunt you, so I chose to have a single place where you set the defaults.
In case you have a setting that can be an enumeration, you can persist it as an integer. Or, if you want to be more user friendly, you need to define a to/from string conversion and register it—you'll do this in ss::init_settings. Note that this will work only if boost::is_enum works. Otherwise, you can use enum_ function; check out Listing Nine.
typedef enum answer { yes, no, maybe }; void ss::init_settings() { def_cfg().add_storage("", file_storage("app.ini"); ... register_enum<answer>() (yes,"Yes")(no,"No")(maybe,"Maybe"); } // Using enums in code // ... if boost::is_enum works answer a = setting("user.default_answer"); setting("user.default_answer") = maybe; // ... if boost::is_enum does not work answer a = enum_(setting("user.default_answer")); enum_(setting("user.default_answer") = maybe;
The initialization of the library, the defaults and the enumerations are all done in ss::init_settings. They could be done somewhere else, like, in main(), or with global variables, etc. The reason I chose to do it here is because you might need to get/set the settings before main() as part of some global variable's initialization routine. As "but you shouldn't do it" as this sounds like, in practice this happens quite a bit: log initialization, MFC initialization, etc.
To persist an array or a collection, you basically need to persist all its elements. For an STL-like non-associative array (std::vector, std::list, etc), simply use the array() function. For an associative array (std::map, etc) use the coll() function. In case you have a fixed C-like array (to which you assign using the a[i]=val; syntax), use the array(), and pass the array's size as the second argument. See Listing Ten.
std::vector<int> a; std::map<std::string,long> b; employee c[20]; array(setting("app.widths")) = a; a = array(setting("app.widths")); coll(setting("app.corresp")) = b; b = coll(setting("app.corresp")); array(setting("app.empls"),20) = c; c = array(setting("app.empls"),20);
Note that both the array() and coll() functions are helpers: they simply read or write the array's size, then generate unique names for each element, and read or write them.
Working with settings, you might end up with exceptions. Here are a few:
By default, the errors are ignored (in the third case, you'll get a failed assertion). Otherwise, you can set your own error handler (set_error_handler), to which you pass a callback function with two parameters: an error code (integer) and the error string.
The library is thread safe, by default (using Windows threads). You can override how it deals with thread-safety issues like this:
By default, the setting names are char-strings. In case you want to override this, just define the SETTING_CHAR macro.
You can download the code from here or from www.macadamian.ro/drdobbs/. It's tested on VC 7.1 and gcc 4.0.
Many thanks to my faithful reviewers: Bartha Bela, Florin Toma, Marius Bucur, Marius Olteanu, Ovi Deac, Ovi Miron, and Ovi Pana.
Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.