Straightforward Settings

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!

Usability: More than Meets the Eye

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?

Test Drive

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;

Listing One

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;

Listing Two

Case-insensitive Names

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;
Listing Three

Runtime Constants

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");
Listing Four

The Dot Makes a Point

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);
  }
};
Listing Five

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
Listing Six

Initialization

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
Listing Seven

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).

Defaults

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");
}
Listing Eight

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.

Enumerations

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;
Listing Nine

The Need for ss::init_settings

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.

Arrays and Collections

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);
Listing Ten

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.

Error Handling

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.

Thread Safety

The library is thread safe, by default (using Windows threads). You can override how it deals with thread-safety issues like this:

Unicode Setting Names

By default, the setting names are char-strings. In case you want to override this, just define the SETTING_CHAR macro.

The Code

You can download the code from here or from www.macadamian.ro/drdobbs/. It's tested on VC 7.1 and gcc 4.0.

Acknowledgements

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.