Users expect a lot of an application these days. The customer wants the application main window to be shown where he left it last time. He wants everything restored just the way it was when he last closed the application: the last document(s) opened, the last database reopened, the last project reopened. And on top of it, he probably wouldn't mind a cup of coffee.
The unfortunate news is that it's your job, my job, and about every programmer's job to actually provide this functionality (well, except for the coffee). Let's consider what we can do to ease the problem. In fact, I'll show you a library that will make you grab the phone, talk to your favorite user and say: "When you go to a Web site you've been to before, do you want the last page to automatically be reopened? You do? Sure thing—my pleasure."
Time to remember
So, you need to remember something after the application is closed. The word persist pops into your head. Persisting is the process of saving some data to an external storage, so that it can be reloaded later. Persisting can take many forms: to a file, to a directory, to the registry, to a Web address, etc.
As your application grows larger, more and more settings have to be persisted; some of your previous constants will become settings.
A persistent setting is some data that needs to be saved when the user closes the application, and restored when the user reopens it. Another twist is that this setting can change during the application run (for instance, the user's email is updated). Any type of data can be persisted (not just strings and/or integer values).
Let's be smart
When persisting, you can choose to either persist the settings in a certain order or assign each setting a unique ID, and therefore be able to find it at any time, as shown in Listing A.
The former solution is very error-prone: You always have to remember the order of the settings, and what do you do when some settings are added or removed. The latter is quite self-explanatory and flexible as well, and is the preferred method.
While you can choose any type for IDs (even integers), experience shows that it is usually best to use strings. They can be made self-explanatory: wnd_left will most likely make you think about the application window left position.
Let's get even smarter: Each setting has a context. Conceptually, it belongs (relates) to something. For example, wnd_left above, relates to the application. So, put it in writing—separate each context by a dot (.). Instead of the plain wnd_left, call it app.wnd.left.
In addition to being even more self-explanatory, it brings another huge advantage: Not all settings need to be kept in the same place. When more settings are needed, based on their context(s), you can choose to store some settings in the registry, while storing other settings in one or more files (i.e., the application-related settings in the registry, and user-related settings in a file).
Of course, a context can have subcontexts. This is easy and straightforward: app.symbols.1min.cache_size (size of cache for one-minute symbols), app.users.max_count (max. users that can logon to the application).
Another advantage is, in cases where you persist these settings to the registry, behind the scenes, you can map dots to registry keys (app.users.max_count -> HKCU/software/mysoft/app/users/max_count).
The library provides access/manipulation to persistent settings; in other words, getters and setters for the settings.
Above all, the interface should be simple. If it's hard for you to use this library, it's most likely you're not going to use it. Here is about as simple as it can get:
- Get: type value = setting<type>(setting_name);
- Set: setting<type>(setting_name) = value;
There are a few examples to get you started in Listing B.
As a side-note, the setting names are case-insensitive. app.wnd.left is the same as App.wnd.Left. As more and more settings are added to your application, it's easier for you and your fellow programmers to concentrate just on contexts (like: app, wnd, etc.), not on how they are capitalized (App, APP, Wnd, etc.).
Keeping thy settings
As I said before, you can choose to keep your settings anywhere you like. It's the library's job to shield you from where the settings are kept.
You can have one or more storages. A storage contains a group of persisted settings, allowing you to get/set them. The storage can be anything: a file, a portion of the registry, etc. The library provides two types of storage classes: a file storage and a registry storage. You can create your own storage class and plug it in. It just needs to derive from setting_storage as shown in Listing C.
At application start, before main(), you have to specify to which storage(s) your settings are persisted. You do this by implementing the persist::on_initialize_default_config function. Here you plug in the storages that keep your settings, simply calling the configuration add_storage method (more on this, later). Listing D contains an example of this, and a few examples of how some settings would—behind the scenes—be mapped to their storage.
What happens if, when getting (requesting) a setting, it does not exist (it's not persisted yet)? The simplest thing is to just ignore this fact and return a default (the default-constructed value). Usually you'll be happy with this. In case you need more, you can specify an error handler. Listing E shows you a few ways to customize error handling. Also, note that the error handling is consistent throughout the library—in case you have an operation that could fail, you can use this mechanism.
By default, the library is not thread-safe. To make it thread-safe, just define the directive (macro) PERSIST_THREAD_SAFE. Note that thread-safety is implemented using boost threads.
This is by far my favorite, and one of the library's strongest points: being able to copy a configuration into another (Note: a configuration contains all your application settings). This might not sound like much, but just think of this scenario:
At the customer site:
- Some settings are stored in the registry, HKEY_CURRENT_USER.
- Some settings are stored in the registry, HKEY_LOCAL_MACHINE.
- Some settings are stored in file(s).
In case some problems occur, replicating this configuration (so that you can test it, and see what's happening) is almost impossible. But not in this case: just copy this configuration into a file. This is a one-liner:
When testing/debugging, you can read your settings from a file, even though at the customer site they are read from multiple storages. Listing F shows you such an example. If the application has no command line arguments, it runs as if at the customer site; otherwise, it loads settings from a file passed as the first command line argument (this of course implies you can have as many configurations as you want). Switching between multiple configurations is a piece of cake! Yup, one step closer to debugger's heaven.
Speaking of heaven, you can copy (export) any settings anywhere (just group them into a configuration). For instance, you can copy all settings (from files, etc.) into the registry in three lines of code. Or, imagine that your application has some settings that need to be kept in the registry, for security reasons. Just ship them as a text file (in the setup kit), and the first time you run the application, copy them to the registry—only three or four lines of code.
After downloading the code, you can take a look at the configuration class. Its purpose is to manage all storages, and to allow copying of a configuration into another. Compile and run the sample application, and see how easy it is to copy the configuration into a file or to the registry.
Advanced: The setting_storage class
The astute reader might have seen in Listing C that setting_storage only has get_setting/set_setting for strings. So how can you get/set any type of setting? Again, our friends, the STL streams, come to the rescue. Any type with an overloaded operator >> and << for streams can be read-from/written-to a stream. Listing G shows what's happening behind the scenes.
The fundamental types already have the overloaded operators implemented. If you have a custom type you want to persist, all you have to do is implement the overloaded operators << and >> for streams. That's all.
This library is usually enough for most applications. It can be extended, though, in several ways. For instance, for Unicode applications, you might want to persist Unicode strings. A solution could be to provide a wrapper class, which escapes non-ASCII characters on write, and unescapes them on read.
The error mechanism can be extended to handle errors on get/set differently (right now, they're treated the same). However, this would just complicate the design, bringing almost no benefits.
On top of my list is extending the library to allow persisting of arrays. One possible implementation is to create a wrapper class over an STL container. Then, treat each of its elements as app.setting.array, app.setting.array, etc.
Not that complicated
Persisting doesn't have to be complicated—this article and the code that accompanies it, just proved it. The full library is available for download. Check it out; it's rather simple and it does the work. As a bonus, you'll enjoy the Copy Configuration feature, bringing you one step closer to your customers.