This is the original GotW problem and solution substantially as posted to Usenet. See the book More Exceptional C++ (Addison-Wesley, 2002) for the most current solution to this GotW issue. The solutions in the book have been revised and expanded since their initial appearance in GotW. The book versions also incorporate corrections, new material, and conformance to the final ANSI/ISO C++ standard. |
Preprocessor Macros Difficulty: 4 / 10With all the type-safe features in C++, why would you ever use #define? Problem1. With flexible features like overloading and type-safe templates available, why would a C++ programmer ever write "#define"? Solution1. With flexible features like overloading and type-safe templates available, why would a C++ programmer ever write "#define"? C++ features often, but not always, cancel out the need for using #define. For example, "const int c = 42;" is superior to "#define c 42" because it provides type safety, avoids accidental preprocessor edits, and other reasons. There are, however, still a few good reasons to write #define: 1. Header GuardsThis is the usual trick to prevent multiple header inclusions: #ifndef MYPROG_X_H
#define MYPROG_X_H
// ... the rest of the header file x.h goes here...
#endif 2. Accessing Preprocessor FeaturesIt's often nice to insert things like line numbers and build times in diagnostic code. An easy way to do this is to use predefined macros like __FILE__, __LINE__, __DATE__ and __TIME__. For the same and other reasons, it's often useful to use the stringizing and token-pasting preprocessor operators (# and ##). 3. Selecting Code at Compile Time (or, Build-Specific Code)This is the richest, and most important, category of uses for the preprocessor. Although I am anything but a fan of preprocessor magic, there are things you just can't do as well, or at all, in any other way. a) Debug Code Sometimes you want to build your system with certain "extra" pieces of code (typically debugging information) and sometimes you don't: void f()
{
#ifdef MY_DEBUG
cerr << "some trace logging" << endl;
#endif
// ... the rest of f() goes here...
} It is possible to do this at run time, of course. By making the decision at compile time, you avoid both the overhead and the flexibility of deferring the decision until run time. b) Platform-Specific Code Usually it's best to deal with platform-specific code in a factory for better code organization and runtime flexibility. Sometimes, however, there are just too few differences to justify a factory, and the preprocessor can be a useful way to switch optional code. c) Variant Data Representations A common example is that a module may define a list of error codes, which outside users should see as a simple enum with comments, but which inside the module should be stored in a map for easy lookup. That is: // For outsiders:
enum Errors {
ERR_OK = 0, // No error
ERR_INVALID_PARAM = 1 // <description>
...
}
// For the module's internal use:
map<Error,const char*> lookup;
lookup.insert( make_pair( Error(0), "No error" ) );
lookup.insert( make_pair( Error(1), "<description>" ) );
... We'd like to have both representations without defining the actual information (code/msg pairs) twice. With macro magic, we can simply write a list of errors as follows, creating the appropriate structure at compile time: DERR_ENTRY( ERR_OK, 0, "No error" ),
DERR_ENTRY( ERR_INVALID_PARAM, 1, "<description>" ),
//... The implementations of DERR_ENTRY and related macros is left to the reader. These are three common examples; there are many more. |