I l @ ve RuBoard |
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, and avoids accidental preprocessor edits, among 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 such as line numbers and build times in diagnostic code. An easy way to do this is to use predefined standard macros such as __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 an important, if overused , 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 CodeSometimes, 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... } The thing about this code, though, is that what we really have is two different programs. When we compile it with MY_DEBUG defined, we get source code that includes output to cerr , and the compiler will check that line's syntax and semantics, too. When we compile it without MY_DEBUG defined, we get a different program that does not include the output to cerr , and the compiler cannot verify the correctness of the excluded code. Usually it is better to use a conditional expression instead of the #define . void f() { if( MY_DEBUG ) { cerr << "some trace logging" << endl; } // ... the rest of f() goes here... } This way, there's only one program to compile and test, the compiler can validate all the code, and the compiler can easily eliminate the unreachable code if MY_DEBUG isn't true. b) Platform-Specific CodeUsually, it's best to deal with platform-specific code using a factory pattern, because this approach makes 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 RepresentationsA 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 Error { 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( ERR_OK, (const char*)"No error" ) ); lookup.insert( make_pair( ERR_INVALID_PARAM, (const char*)"<description>" ) ); ... We'd like to have both representations, without defining the actual information (code/message pairs) twice. With macro magic, we can simply write a list of errors as follows , creating the appropriate structure at compile time: ERR_ENTRY( ERR_OK, 0, "No error" ), ERR_ENTRY( ERR_INVALID_PARAM, 1, "<description>" ), ... The implementations of ERR_ENTRY and related macros is left to the reader. These are three common examples, but there are many more. Suffice it to say that although the C preprocessor should be avoided in many places, it still has its share of useful features that can, when used judiciously, make writing C++ code easier and safer. Guideline
To see what the inventor of the C++ language thinks of the preprocessor, check out section 18 of [Stroustrup94]. |
I l @ ve RuBoard |