The #define directive in C++ is usually glossed over in most books that attempt to teach the C++ language. It obviously takes much lower precedence than more important language constructs like if and switch. However this directive deserves far more credit than it gets. Here are some of the feats this directive can perform.
#define can help in reusing code and writing cross-platform applications. Plus it can help you get rid of a lot of broilerplate1 code and provide a host of useful macros that can be used over and over again.
There are three ways you can use #define:
- #define token [value]
- #define token() value
- #define token([param1, param2, ...]) (c++_code)
The #define tokens have global scope during compilation. They get defined when the compiler first encounters them and once defined they remain defined across files. Their scope only ends when the compilation completes or when the compiler encounters a explicit #undef directive for the token.
1. #define token [value]
This syntax is what most programmers are already familiar with. It is also frequently misused to do things like:
#define PI 3.141
when actually what should be done is2:
const double PI=3.141;
Avoiding double inclusion of headers
The most frequent use of this syntax is for avoiding double inclusion of a header file. Start your header files with something like:
#ifndef _myHeader_H #define _myHeader_H // code ... #endif
Here the [value] for token _myHeader_H is empty. Defining the token is simply a means of instructing the compiler to skip to the end of the #if, if the token has already been defined (because the header was #included previously), effectively preventing header inclusion twice.
Another way this syntax is used is for inserting additional code (print statements, asserts) that you want to include only in a debug build of an application.
this can be done by enclosing the debug code in
#ifdef _DEBUG_ // debug code ... #endif
Most compilers even allow you to define the _DEBUG_ token on the command line.
The main hindrance when writing code that can compile on different platforms is that the API used by the platforms may not be the same. While the standard C++ libraries try to overcome most differences, they may not suffice.
That does not stop the determined programmer though:
#ifdef _WIN_ // windows code ... #else // unix code... #endif
2. #define token() value
This second form is a clever way of declaring something like a key and value pair. If token is defined, then token() is its value.
#define class_name() myClass #ifdef class_name // Use class_name() to return the defined value #define __the_class class_name() __the_class* object = new __the_class(); #undef __the_class #endif
The compiler will compile the above code into:
myClass* object = new myClass();
Note that to really benefit from this type of defines, it is recommended that you place your #ifdef code in its own header file and include the header file immediately after defining the value3. So the above example is better written as:
#define class_name() myClass #include "includes/classcreator.h"
Where classcreator.h contains:
#ifdef class_name // Use class_name() to return the defined value #define __the_class class_name() __the_class* object = new __the_class(); #endif
3. #define token([param1, param2, ...]) (c++_code)
Finally, this third form is useful for creating macros.
For example, a macro that will blindly cast the supplied object pointer to the specified type:
#define STATIC_CAST(TYPE, OBJPTR) ((TYPE*)(OBJPTR)) baseClass* obj = new myClass(); myClass* derived = STATIC_CAST(myClass, obj);
If you are wondering about the seemingly unnecessary extra parenthesis around TYPE* above, you should read about precedence.
Again good coding practice dictates that macros be placed in a separate header file and this header file can be included at the top of any source that needs to use the macros.
A word of warning
A important thing to remember about macros is that they are an advanced programming construct and should only be used if you are trying to solve a problem that cannot be otherwise solved using base classes, functions or inline functions. Horrible programs are written when programmers overuse a newly learned programming concept to solve all possible programming problems. So if you can't truly justify your use of a macro to solve a problem, you probably should not be using one.
- 1. In information technology, a boilerplate is a unit of writing that can be reused over and over without change.
- 2. #define will substitute the literal 3.141 each time you use the token PI. This means that the compiler will internally allocate memory and fill it up with the value 3.141 each time token PI is used, compare this to the const variable PI where the variable is allocated and filled up only once.
- 3. This usage-pattern is also called X-Macros.