“Use constants in code!” Everybody tells you this, and they’re right. The trouble starts when you realize that some constants are lost at compile time—they must be read at runtime (after the program starts), from a file, registry, database, etc.
Problems can also strike while you’re testing and debugging your application, or when you need to stress test some part of it. You’ll have to tune some constants’ values, and you could end up with a plethora of configurations (Listing A). Even worse, try profiling your application. You’ll have to run it multiple times with different values for constants, because changing one value will require recompiling your application.
In this article, I’ll show you several ways to change from compile-time constants to runtime constants. The reverse process is as easy—and this is important, since after profiling, you might decide to keep some compile-time constants to make your application run faster.
You’ll see that changing a constant from compile-time to runtime or back will require you to change only one line of code—its definition. But even so, the process is far from simple. Grab your coffee and get focused, because it’s going to get tough.
Runtime vs. compile-time
A compile-time constant is a value that can be (and is) computed at compile-time. A runtime constant is a value that is computed only while the program is running. If you run the same program more than once:
- A compile-time constant will have the same value each time the application is run.
- A runtime constant can have a different value each time the application is run.
Note that once initialized, neither type of constants can change its value—they’re constant. For example:
const int COMPILE_TIME = 5; // set at compile-time; its value is 5 (five)
int read_int() { int val; std::cin >> val; return val; }
const int RUN_TIME= read_int(); // set at runtime; can be any value
Transforming a compile-time constant into a runtime constant is not as easy as it looks because you can’t control the time at which the runtime constant gets initialized. Besides, a runtime constant, once initialized, should not be able to change its value.
How const is const?
There are a few cases where runtime constants just won’t do and compile-time constants will be required. Knowing what these cases are will save you a lot of trouble. Even if you declare a variable as constant, if you initialize it only at runtime you can’t use it for:
- Array bounds
- Case expressions
- Bit-field lengths
- Enumerator initializers
- Integral or enumeration nontype template arguments
Table A shows examples of each.
Table A
|
Also, you should note that some optimizations won’t happen when a compile-time constant is turned into a runtime constant, as shown in Listing B.
The solutions
I’ll walk you through the final solution step-by-step. As implementations get more and more complicated, they will require multiple files and lots of lines of code. I’ll highlight the key portions of code as I go along; you can download the complete files from TechRepublic.
Solution 1
First, transform compile-time constants into runtime constants. You’ll start by allowing for properties objects. A properties object contains properties; you can ask for a given property of a given type. Properties can be persisted to a file, registry, etc., and the location is transparent to the programmer. Listing C and Listing D show an implementation that keeps each property on a line like this: <prop_name> prop_value.
The simplest solution is to just initialize constants from a properties object, like this:
// instead of ‘const long MAX_USERS = 100;’
const long MAX_USERS = get_prop< long>( “max_users”);
After examining Listing E, you should note several aspects:
- app_props.h and .cpp allow access to the properties that are read from file props.txt.
- AllUsers.h and .cpp define a simple class; in its constructor, you access MAX_USERS, a constant that is initialized at runtime.
- main.cpp defines a global (s_AllUsers) of type AllUsers.
- props.txt contains some properties (max_users and max_fails).
Before running Listing E, keep in mind that global variables, such as s_AllUsers and MAX_USERS, are initialized before main().
By running the example in Listing E, you can see the problem: AllUsers’ constructor accesses the MAX_USERS (global) constant. To the compiler, both s_AllUsers and MAX_USERS are global variables that are initialized at runtime. You do not know the order of initialization of global variables, so you can’t know which is initialized first. If s_AllUsers is initialized before MAX_USERS, it will see MAX_USERS as 0 (instead of 1,000), printing “there are max 0 users that can login.” You certainly don’t want that.
Notice that problems occur only when global variables use global constants (in this case, s_AllUsers uses MAX_USERS) because you can’t know whether you’re using a constant before its initialization occurred. If a global variable uses a local constant, there’s no problem, since the local constant is initialized prior to being used (Listing F). From now on, when I refer to constants, I will always refer to global constants.
Solution 2
Solution 1 gave you a clue: Distinguish between constants and globals that depend on them. The constants are initialized as before, and the globals that depend on them are initialized afterward. But how do you know when to start initializing the dependent globals?
This is close to impossible. But you’re sure that when you are operating in main(), all the constants have been initialized because they are global variables. Therefore, you can safely initialize the dependent global variables.
For a dependent global, you postpone its construction by buffering all its constructor arguments. Then, when you are inside main(), you can safely construct it. You need change only one line of code:
DbConnection conn( “(local)”, 5); // old
dependent_static< DbConnection> conn( “(local)”, 5); // new
As easy as this solution sounds, it’s quite complicated to translate into code and took more than 2,000 lines. Here are some of the highlights:
- Create a templated class (collect_parameters) that buffers all the arguments that are passed to it at construction.
- Create a templated class (dependent_static< type>) that uses collect parameters to collect the arguments passed at its construction. When initialize() is called, it creates the underlying variable passing the buffered arguments to its constructor.
- Call all_dependent_statics::initialize_statics() in main() to initialize all dependent statics.
Listing F shows how collect_parameters and dependent_static classes look for up to two parameters. The final solution allows for up to 20 arguments. Using these, you’ll only need to do the following changes in main.cpp and recompile:
#include “dependent_static.h”
dependent_static< AllUsers> s_AllUsers; // used to be AllUsers s_AllUsers;
all_dependent_statics::initialize_statics(); //place it after int main() {
Before moving onto the next solution, realize that you can initialize your constants from multiple places: some from file(s), some from a registry, or from wherever you like. Just implement your own properties class.
Solution 3
Previous solutions force the constants to be initialized before main(). You can’t initialize a constant within main(), since you can’t change its value. This is a major drawback because it forces the properties object you read from to be initialized prior to initializing the constant:
static properties props;
// before MAX_USERS is initialized, props must be initialized!
const long MAX_USERS = props( “max_users”);
But what if you need to initialize all constants from a file whose name is a command line argument? In this case, you need to initialize the constants after entering main(). You’ll use a technique similar to the one presented above, which includes these aspects:
- Create a templated class const_val< type> representing a constant. (Client code uses it just like it were const type.)
- Pass the properties object and the property name you want to use to initialize it at its construction.
- Initialize the constant when initialize() is called from the properties object (using the property name).
- Call all_constants::initialize_constants(), which will call initialize() for all existing const_val constants in main().
If you try to use a constant or a dependent_static before it’s initialized, it throws. This helps, in case you forget to mark a static variable as dependent. If such a variable uses a runtime constant, you will catch this error when you run first run Listing G.
You should note that if a global variable’s constructor throws an exception, you can’t catch it—your application will terminate (Listing H). However, this will never happen in this case, since you initialize the global variables only inside main(). You can even surround the initialization in a try/catch block and end the application more gracefully, as shown in Listing I.
Take a look at Listing J. You now have a new dependent static s_dbConnection. Note that there are two property files: props.txt & props2.txt. Pass each of them as an argument to the program and see what happens.
Solution 4
This solution will ease using const_val and dependent_static. When constructing a const_val, the property name conceptually belongs to the properties object. It’s more straightforward to say
const_val< long> C( get_prop( “name”) )
than
const_val< long> C( get_props(), “name” )
This solution will allow for that.
If a constant can’t be initialized (the property does not exist for the properties object), it can be given a default value. Just add an extra argument:
const_val< long> MAX_USERS( get_prop( “max_users”) ); // old
const_val< long> MAX_USERS( get_prop( “max_users”), with_default_val( 1000) );
Initializing a constant could fail, in which case the property does not exist; this will throw an exception. If you provided a default, the constant will be set to that value and the program will continue.
To initialize both constants and dependent_statics, call constants_and_dependents::initialize instead of all_constants::initialize_constants() and all_dependent_statics::initialize_statics().
You can now manage dependencies among dependent_static objects. If dependent_static A depends on dependent_static B, you will initialize A after B.
Consider s_AllUsers from the previous solution, which needs to output some data to an s_statisticsLog. Both s_AllUsers and s_statisticsLog are dependent_statics. For your application to function correctly, you need to initialize s_statisticsLog prior to AllUsers. Adding dependencies is fairly easy:
- Give B a name
- Add a dependency from A to B’s name
Here is an example:
// Logs.h
dependent_static< statistics_log> s_statisticsLog;
set_dependent_static_name statsname( s_statisticsLog, “statistics”);
// main.cpp
dependent_static< AllUsers> s_AllUsers;
add_dependency_on_static dep( s_AllUsers, “statistics”);
Both set_dependent_static_name and add_dependency_on_static are helper classes, whose only purpose is to give a name to a dependent_static or create a dependency.
In >Listing K, you’ll see two FIXME comments. If you run the program as is, you’ll probably get an exception. Uncomment those two lines, and the program will run as expected.
Solution 5
Try the following:
const_val< char*> USER_NAME( get_prop( “user_name”));
It will fail miserably, because const_val internally holds a char * m_val. Initializing USER_NAME from a properties object will copy a null-terminated string to m_val. However, m_val is NULL and an access violation occurs. You need to allocate memory for m_val, which should be deleted at const_val’s destruction.
One approach is to internally hold a std::string and to the outside world, provide a const char* conversion. This is just what you’ll do—for raw char* strings, you internally keep std::strings. Solving this requires traits, which you can extend to match your classes.
In the previous solutions, dependent_statics provided for postponing initialization of static variables. The dependent_statics variable is automatically initialized when constants_and_dependents::initialize() is called. However, a dependent_static is not required to be global. The code shown in >Listing L will fail.
In contrast, const_val constants can only be global. If you want a constant variable to be local to a function you don’t need const_val. For a local variable, you don’t want to postpone its initialization. As a matter a fact, you want to initialize it right away by using get_prop_val:
int f() {
const int MAX_USERS = get_prop_val< int>( get_prop( “max_users”));
// …use MAX_USERS
}
That’s it. Check out >Listing M for the complete final solution.
Advanced topics
When transforming a global/static variable into dependent_static< type>, you don’t usually have problems. However, there are a few cases when the transition is not as straightforward:
- When you want to pass a reference to an object
- When you want to pass an object, and its class has no copy-constructor
- When you want to pass a const_val or another dependent_static
The cause of all of the above is the fact that dependent_static takes all parameters by value. If you want to pass a reference, you should use a reference class, like boost::ref/cref.
If you want to pass an object whose class has no copy-constructor, a compile-time error will occur. This is why you should pass that object by reference. When you want to pass a const_val or another dependent_static, you should note that they are not copy-constructible, so you must pass them by reference. >Listing N shows examples of each reference pass.
Conclusion
Changing compile-time constants to and from runtime constants should be simple, but it’s far from that. That’s why I developed the const_val and dependent_static pair—they bring simplicity to this process. Here’s a final recommendation: First, always set default values to the runtime constants. Then, transform as many constants from compile-time to runtime as possible. That way, you’ll have multiple sets of constants to choose from at runtime, enabling you to test/debug/profile your application more easily.