The preprocessor only does text substitutions. This could potentially replace something unexpected, such as part of a longer variable name. Also, as mentioned above, preprocessor directives can propagate across source files easily and often without warning. knowledge that adding a simple header file. Last, but certainly not least, they circumvent the type system.
Header files that provide equates using preprocessor directives are very common. For example:
1 2 3 4 5 6
|
// this is frowned upon
#define REQUEST_INVALID (0)
#define REQUEST_DO_THIS (1)
//...
#define REQUEST_DO_THAT (100)
#define REQUEST_MAX (101) // must be last
|
Subsequent code may store these request equates as unsigned integers, or some other integral type. Note that inserting a request in the middle of the list forces the developer to renumber the rest of the list. Now consider a better solution:
1 2 3 4 5 6 7
|
enum Request {
REQUEST_INVALID = 0, // invalid should always be zero to support initialization to zero and conditional checks
REQUEST_DO_THIS,
//...
REQUEST_DO_THAT,
REQUEST_MAX // must be last
};
|
Enumerations can do the counting for you, do not have the drawbacks of preprocessor directives, and, in C++, can be type checked. Subsequent code may now use the Request type rather than an unsigned integer and the compiler will generate an error when an unsupported value is used. One more small caveat is that version control systems can merge additions automatically rather than requiring a manual merge when two developers insert differing requests at the same time.
Again, as mentioned above, consider providing contants in the smallest scope possible. In C++, module-scope constants can be defined in an unnamed namespace:
1 2 3 4 5 6 7
|
// the.cpp
#include "the.hpp"
//...
namespace {
const double PI = 3.14159;
}
//...
|