Fortran IV let you read and write variables by name since the 1960s. It still makes sense to do so in C++.
Introduction
Have you ever wished that, within a program, you could obtain a variable name as a string? Then you could easily display variable names along with their values, or you could read or write variables to .INI files [1] using the actual variable name as a key. This would be particularly useful when dealing with global parameters, such as process parameters in a machine controls application or options in a general purpose application.
In this article, I introduce a template class NamedVal, that wraps simple data types and stores the variable name as a string. A preprocessor macro takes the variable name as an argument and uses the stringizing operator to generate the name string. I show how several NamedVal objects can be grouped into a structure I call a parameter block. The parameter block can then be used to build a simple but effective I/O mechanism that reads or writes NamedVals to a simple database using the variable names as keys. I also show how the database implementation details can be kept separate from the NamedVal implementation details.
Finally, I use a parameter block to generate a dynamic dialog box. The dialog box displays the variable names next to edit fields that enable users to display and change the values. The layout of the dialog box is automatically generated based on the contents of the parameter block. Putting all the pieces together, I end up with a data structure that acts like a regular C++ structure, but can also load, display, edit, and save the member values.
Designing a NamedVal Template
It was the frequent use of strings to refer to parameters in my machine vision applications that motivated me to design the NamedVal template. Before I began using NamedVal, my code contained many lines such as:
GetPrivateProfileString ( section, "CameraPort", "Port0", camPortStr, sizeof camPortStr, iniFileName )and somewhere else:
WritePrivateProfileString ( section, "CameraPort", camPortStr, iniFileName )which were tedious and error prone to write. (For readers not familiar with Windows programming, the above are two functions Windows provides to read from and write to an .INI file [1].)
Of course I didn't have to use string literals instead, I could define parameter strings at one central point in the program. In the past, I have used a table that maps parameter strings to the variables holding the parameter values. The table approach works well, but I still want a more elegant solution a solution that directly links the values to their names and can be used like a built-in type. I want to have the variable itself "know" its own name. This has led me to try a template-based wrapper class.
Given the proper conversion operators, a C++ class can be used wherever a value of the wrapped type is required. This automatic conversion capability is the first feature the new class should have. The other feature is a string that represents the name of the variable. This string should contain the C++ identifier; that is, a variable named numParts should contain the string "numParts". A class that has both the necessary conversion operators and the name string member is shown in NamedVal.h (Listing 1).
Note that NamedVal's constructor requires a pointer to char to initialize the embedded name string. At first glance, it seems that I have simply moved the problem of associating the string and variable to the wrapper class constructor. This is where the preprocessor comes in: the key is the stringizing operator, #, which turns a macro argument into a string. Although I usually try to avoid using macros, I believe this is a case where only a macro can solve the problem.
Helper Macros
The macro:
#define foo(arg) printf(#arg)when instantiated with the argument bar:
foo(bar);causes "bar" to be printed. In my macros, the variable name shows up only once as a macro argument, but is used both to define the variable and the variable name as a string. For example, writing
DEF_NAMED_VAL(double, Pressure)will define a NamedVal Pressure with the name member set to "Pressure".
Now if you write:
Pressure = 123.45;you can pass Pressure to a function that takes a double argument, or you can print the value of Pressure to cout:
cout << Pressure.Name() << "=" << Pressure << endl;which will cause the string "Pressure=123.45" to be printed.
If you need to create NamedVal with an initial value, you use a modified version of the macro with an additional formal parameter. Using
DEF_NAMED_VAL_I(double, Pressure, 23.4);will initialize the NamedVal Pressure to 23.4. Having to use different macros to call the constructor with an initial value is a drawback to using the preprocessor. Since you cannot specify default parameters in macros, you have to use a different macro for different argument lists.
It is also possible to dynamically allocate NamedVals from the heap. To do this, you use the macro DECL_NAMED_VAL(int, * namedIntPtr) where you would normally declare a pointer to an int. Then you use
NEW_NAMED_VAL(int, namedIntPtr);to allocate and construct the value.
The final macro, CONSTRUCT_NAMED_VAL, is used when a NamedVal is a member object of a structure. In this case, it is necessary to call the member-object constructor in the initializer list of the structure that contains the NamedVal.
Listing 2 shows examples of using NamedVals in different ways, as local and dynamically allocated variables and as members of structures.
Adding I/O Capability
To show how NamedVal can simplify reading and writing parameters to simple databases, such as .INI files or the Windows registry, I first introduce a fictitious database. SimpleDB serves as a stand-in, for either an .INI file or the Windows Registry. This fictitious database has only two operations: reading a string from or writing a string to the database. Each string value is uniquely identified by a section and a key. Typical examples from my application domain are section="InspectionStation1", key="SPC_Port", and value="COM2". Most Windows users are familiar with this type of simple database. To make the test examples work, I define mock Read and Write functions that read from cin and write to cout.
As stated in the introduction, one of the design goals was to use NamedVals in a block of parameters. The most obvious way to hold multiple NamedVals in a block is with a Standard C++ container, such as std::vector. However, to hold multiple NamedVals in a vector, the NamedVals must all be of the same type; alternatively, if all NamedVals derive from a common base class, the vector can hold pointers to them (as pointers to their base class).
Adopting the latter approach, I first define an abstract base class IOAble, which defines just the interface to the database:
class IOAble { public: virtual void Write(SimpleDB &db, const char *section)=0; virtual void Read(SimpleDB &db, const char *section)=0; };It is now possible to define a class IOParamBlock, which contains a std::vector of IOABle *. An IOParamBlock can iterate through all the vector elements and perform reads and writes on the IOAble interface. Of course, IOAble is still an abstract base class, which cannot be instantiated. The next step is to make NamedVal a derived class of IOAble. This makes it possible to insert a pointer to any specialization of the NamedVal template into the vector of IOAble *.
Note that NamedVal::Read and NamedVal::Write convert the val member (the actual value of the variable) to a string in order to call the Read and Write functions of SimpleDB. Other databases might have overloaded Read/Write functions for different datatypes that could directly accept the val member in NamedVal. The file NamedVal.h (Listing 1) defines the classes SimpleDB, IOAble, and IOParamBlock.
To use NamedVal as part of an IOParamBlock, you have to use the third version of the NamedVal constructor, which takes a pointer to the IOParamBlock containing the NamedVal. Rather than calling the constructor directly, however, you use the macro CONSTRUCT_AUTOIO_VAL (see Listing 1). Consider the class TestBlock in Listing 3: TestBlock's constructor calls the constructors of autoInt and autoFloat, which are both members of TestBlock. The macro CONSTRUCT_AUTOIO_VAL passes TestBlock's this pointer to the NamedVal constructor. The NamedVal constructor, which expects an IOParamBlock * as a parameter, then calls IOParamBlock::AddVar via the this pointer that was just passed in. Once all the constructors of the TestBlock's member objects have been called, TestBlock will contain a vector of pointers to its NamedVal member objects. Figure 1 shows the class hierarchy for NamedVal, IOAble, IOParamBlock, and TestBlock.
One of the two compilers that I tested this code with warned about using TestBlock's this pointer in a constructor of its member NamedVal object. However, base class objects (in this case, of type IOParamBlock) are constructed before member objects. It is okay for those member objects to reference the base class (via its this pointer) as long as they call no virtual functions. In this case, it is okay for the member objects of TestBlock to call IOParamBlock::AddVal through TestBlock's this pointer, because AddVal is not a virtual function. For an explanation of using the this pointer in the constructor, refer to the C++ FAQ Lite [2].
All this might seem a little confusing, but the net result of setting up the NamedVal class as described can be seen in the example class TestBlock (found in Listing 3). Once you define TestBlock using DECL_NAMED_VAL and CONSTRUCT_AUTOIO_VAL, you can call the member functions ReadAll and WriteAll to read and write all members from and to the database.
Adding Notification Using the Observer Pattern
In many applications, the value of a parameter in one object may be important to several other unrelated objects. These objects will need to be notified when the parameter changes. The Observer pattern [3], also known as Publish-Subscribe, is a simple abstraction of the solution to this problem. There are many ways to implement the Observer pattern. In one implementation, both the subject (which contains the parameter) and the observer are derived from base classes. Listing 4 shows the definitions of these base classes. The Subject base class keeps a list of attached observers. When Subject's Notify method is invoked (indicating that a parameter has changed in a derived-class subject), it notifies each observer by calling the observer's Update method. The observer checks which of one or more subjects has changed and carries out whatever action is necessary.
In my applications, the subject is typically a class derived from both IOParamBlock and Subject; whoever creates a change in this subject is also responsible for calling Notify. Listing 5 shows definitions of these typical subject/observers classes.
Creating Dynamic Dialog Boxes
Before I developed NamedVals and IOParamBlocks, I built dialog boxes the standard way: I used the GUI editor built into Microsoft Visual C++ along with ClassWizard. It is very easy to build dialogs this way, but there is one very annoying aspect. ClassWizard forces you to create member variables of certain types, which will be linked to the edit fields, buttons, or other GUI elements. There is no way of telling ClassWizard to link a member of a structure or array to a GUI control. Say you want to add another parameter to an already existing set, for which you have created a dialog template and a class derived from CDialog. Now you'll have to add the GUI controls, the new member value, and, if you're loading and saving the values from an .INI file, you'll have to add more code to do the reading and writing.
If you're not very particular about the layout of the controls on the screen, you can use an IOParamBlock to automatically generate a dialog box. This dialog box displays the current values of all IOParamBlock members and writes them to the .INI file when the user clicks OK. Dialog templates or classes derived from CDialog are not necessary to do this. I also use only standard Windows API calls, so you can use the dynamic dialog boxes in non-MFC applications.
The code to generate a dynamic dialog, based on the members of an IOParamBlock, is quite simple. Listing 5 shows how it is done. The static controls containing the variable names and the edit fields for the variable values are created in the WM_INITDIALOG handler. First, however, there is one hurdle to overcome: the IOParamBlock only knows about IOAble pointers, and an IOABle can only Read or Write its value to a SimpleDB. To display the names and values of the NamedVals to a dialog box, IOParamBlock would need to have direct access to its NamedVals.
Instead of adding forwarding functions to IOAble, I decided to create a class DlgDB, which is derived from SimpleDB. You can think of the dialog box as a type of database: the Write function creates the GUI controls and puts the current values in the edit box; the Read function reads the current values from the edit box. DlgDB keeps a std::map<string,HWND> to associate the name of the variable with the handle of an edit box. DlgDB is also responsible for a very simplistic layout algorithm. Each call to Write increments a row counter. The row coordinates of the GUI controls are based on the row counter. Note that a real application would go to greater lengths to ensure that the layout would be right for different font sizes, perhaps also taking the real lengths of the variable names into account.
The last piece of the dynamic dialog box mechanism is found in the WM_COMMAND handler for IDOK. This code is executed when the user clicks OK; it transfers the values from the edit fields into the members of IOParamBlock. IOParamBlock::ReadAll is responsible for the transfer. In order to call ReadAll, you have to retrieve the pointer to the original IOParamblock. The WM_INITDIALOG handler previously stored that pointer in the OK button's user data.
Listing 5 also shows how the Observer pattern integrates with the dynamic dialog box. In this example, the Notify function gets called only when the user clicks OK, which means that new values are copied into the NamedVals. The call to Attach in the Test function would normally appear in the constructor of the observing class, and the call to Detach would normally appear in the destructor of the observing class.
Summary
In this article, I have introduced a class hierarchy that simplifies handling of named parameter variables. The classes in this hierarchy allow reading and writing the variable values keyed by their identifiers. I have also demonstrated how dynamic dialog boxes for user modifiable parameters can be created. Finally, I have shown how the Observer pattern can be used to notify clients of the parameter variables.
References
[1] .INI files are simple text files on Windows-based systems that serve as simple databases. They are organized as key-value pairs of strings.
[2] Marshall Cline. "Should you use the this pointer in the constructor?" , Topic 10.7, C++ FAQ Lite, http://www.cerfnet.com/~mpcline/c++-faq-lite/.
[3] Erich Gamma et al. Design Patterns: Elements of Reusable Object-Oriented Software, (Addison-Wesley, 1995).
Andrew Queisser got his degree in Mechanical Engineering from Stuttgart University, Germany. His areas of expertise are image processing and machine vision. He works at Hewlett-Packard in Corvallis, Oregon and can be reached at ahqueisser@aol.com.