Adrian Barbu has an M.S. in software engineering and is a project manager at Goto Informatique, a French software company specializing in communication software. He can be reached via e-mail at 100125.237@compuserve.com.
Introduction
Writing a C++ class from scratch has always seemed like a tedious and error-prone business to me. I've found it difficult to bridge that gap between my mind, holding fast to a new concept at one end, and my fingers, typing in code at the other end. Moreover, as experience and good C++ books compel me to make more and more class design decisions, I find increasing need for a tool that allows me to focus on essentials from the very beginning.I expect such a tool to save me more than some brainless typing; I also want it to work as a repository of canned design experience, ready to be converted to generated code in a mouse click (see sidebar for examples). Want to forbid the use of a (frequently backfiring) default copy constructor? So be it, just click the right button and it's gone that's the kind of tool I'm looking for.
Such a feature may seem to promote overly cautious programming, but I like it that way. On the other hand, the tool's design repository must remain as flexible and open as possible throughout its life span, allowing it to absorb new features or to be molded into some other shape, without requiring modification to the class generator's core code.
The tool that reasonably answers these needs turns out to be a simple C++ class generator based on pattern files. This generator creates the skeleton of a class interface (.HPP file) and definition (.CPP file). It is implemented as a line-oriented substitution engine, with a user interface based on MS-Windows 3.1 or later (see Figure 1) .
What Are Pattern Files?
Pattern files are text files that behave somewhat like big macros, which will be transformed into C++ code. The class generator uses two mechanisms, substitution and conditional code generation, and these mechanisms are controlled by the pattern files. The two pattern files are __GEN__.HPP (Listing 1) and __GEN__.CPP (not shown, but included on the code disk).Using the macro analogy, these files take one argument, the name of the new class itself. This class name is restricted to eight characters, as it is also used to generate output .HPP/.CPP filenames. To stay away from heavy grammars and parsers, I've designed the pattern files to be interpreted on a simple line-by-line basis. The pattern files also include a couple of comfortable metasymbols to isolate the formal arguments from the C++ text itself. Thus, [CLASS] stands for the class name.
For example, if MyClass is the name of the class whose backbone is to be generated, the generator will output files MYCLASS.HPP and MYCLASS.CPP, translating the pattern line
[CLASS]& operator=(const [CLASS]& y)to
MyClass& operator=(const MyClass& y)In this case, substitution works fine within each line.What about conditional code generation? Notice that pattern lines are grouped into blocks. These blocks are delimited by meta-lines, prefixed by #[ and #] (like C's #ifdef and #endif directives). Whenever the generator reaches the beginning of a block such as
#[ CountInst= 1 static unsigned long_HowMany; #]it will decide whether to generate the enclosed block, based on the value of symbol CountInst. Since the user interface just collected values for all these switches a mouse click before, the generator has the information it needs to generate the output class as tailored by the user.As the two central ideas above work on a line-by-line basis, they both rely heavily on substring search operations. This is where STR makes its appearance (Listing 2) .
STR is an old string class I began writing as an exercise back in my early C++ days. I eventually enhanced it with all kinds of stuff that seems excessive in a string class, except when you really need it: concatenation without space trouble, comparisons, letter case tricks, whitespace discarding (functions noFrontSpace and noTrailSpace), and, of course, substring search (functions hasin, with arguments defaulting to case-sensitive search from the beginning).
STR even includes a "given-size" constructor STR(int), an admittedly horrible feature that I use later to build an automatic STR before passing it as a buffer to sprintf or fgets. STR does all the dirty work in the heart of the generator's read-eval-print loop, albeit not without some direction from an elder brother, which I present next.
An Abstract Generation Engine
The ENGINE class (Listing 3 and Listing 4) manages the whole generation job, and thus has several different responsibilities. First, ENGINE handles the substitution process at line level within the private function _substitute, which takes as a parameter the name of the class to be generated. _substitute places that class name in the output text whenever it encounters a substring matching the contents of ENGINE::_Class in the pattern file. (In this case ENGINE::_Class is a const string equal to "[CLASS]".)Second, the central while loop in _fileJob iterates through all the lines of the input file. This process is more complex than merely calling _substitute for each line, as meta-lines controlling conditional generation get in the way. Whenever a meta-line is found (heralded by the occurrence of a substring matching ENGINE::_FlagOn, or #[, analysis goes ahead on the line to pick up the symbol name and its expected value for the block to be conditionally generated.
At this point, the generation engine must compare this value against the actual value chosen by the user. However, as we cannot reasonably expect the ENGINE class to handle the user interface directly (nor have I seen a car engine read the gas price on its own), the job of storing and retrieving the actual user options falls to an abstract agent called SYM, shorthand for "symbol table."
The SYM class mediates between ENGINE and the user interface. SYM presents a pure virtual interface to ENGINE, providing it access to member functions get and set. These functions enable an ENGINE object to read from and write to a Windows .INI file, which will contain the user's selected options by the time ENGINE does its work. ENGINE gets a pointer to the SYM object currently on duty as an argument of the function ENGINE::go. Note, however, that SYM is an abstract base class, so this SYM object must be of a descendant class to SYM. (You cannot instantiate an abstract class.) In a later section, I present SYM's descendant class, WSYM.
Nested Conditional Generation
Since it would be a pity to forbid nested conditional blocks in pattern files, ENGINE must keep track of possible outer blocks when encountering a _FlagOn substring. Of course, the actual value of an option is meaningless if it's already within a block that ENGINE decided not to generate. But the occurrence of a _FlagOn must be paired with its corresponding _FlagOff. The corresponding _FlagOff will not necessarily be the next one in the input file, as the current block might have nested blocks of its own.To cope with nested blocks, ENGINE::_fileJob instantiates a stack of 0/1 values called BSTACK, a class trivial enough to be left unlisted here, featuring classical primitives such as push, pop and top. _fileJob also interrogates BSTACK::empty at places where unbalanced markers are likely to be detected. Once again, design issues get a bit ugly as time goes on; ENGINE should provide some feedback to the user as it is processing all these files and markers. After all, you would find ENGINE quite rude if it overwrote an existing output file without asking permission first. And you would probably rather be warned about a misplaced marker than play around with your freshly generated C++ nonsense.
Again, since I did not want ENGINE involved in user interface issues, I left it as an abstract class, providing it with pure virtual functions to bridge the user interface gaps. For example, ENGINE itself doesn't care how some real overwriteQuest(const char szOutputFile[]) will manage to prompt the user for an answer, but it knows there will be a yes or no answer for it to act accordingly.
The Presentation Layer
Classes WENGINE and WSYM (Listing 5) are missing links, performing tasks under MS-Windows that base classes ENGINE and SYM do not want to mess with. WENGINE knows what a window handle is, and actually accepts one in its constructor as a parent window for message boxes. While extremely simple for the time being, WENGINE is designed to keep future evolution in one place, in the event that user level improvements are made, such as: internationalization support, keeping a history of generated classes, and integration in some larger CASE tool. As for WSYM, it is a true masterpiece of lazy programming, since it is no more than a wrapper for Windows API functions reading and writing integer values from a .INI file.Now, the recipe to cook a simple class generator is as follows:
1) Run a Windows dialog to collect the name and the path of the class to be generated, and the class generation options (via checkboxes and radiobutton groups)
2) Create a new WSYM object pSym and call pSym->set to store each option value
3) Create a new WENGINE object pEng with the two pattern filenames and the dialog's window handle as constructor arguments
4) Call pEng->go with the target path, class name, and pSym as arguments. Translated into plain Windows code, the above takes up some 120 lines of C++ (no MFC, OWL, or any other bird). Full source code for the project is available on the code disk and compiles under Borland C++ 3.1.
References
[1] Scott Meyers. Effective C++, 50 Specific Ways to Improve Your Programs and Designs (Addison-Wesley, 1992).