22.15 Preprocessor directives

Compiling a file is actually a multi-step process. Before actually compiling any code, a compiler uses something called a 'preprocessor' to look through the project files and carries out the instructions embodied in the various preprocessor directives. Any line of C++ that starts with # is a preprocessor directive. Some of them are #include, #define, #ifdef, #endif, and #pragma. Each preprocessor directive tells the compiler to do something before running. The instructions in the # directives are used to alter the contents of the file, which is only then passed on to the normal compilation process.

The #include directive

What the #include directive tells the compiler to do is to replace a line like #include "whatever.h" with a full, exact copy of the Whatever.h file, just as if you had used your text editor to block copy the whole Whatever.h file and paste it in.

If the filename after #include is in quotes, the compiler will look for the file in the directory where the project file lives; and if the include file is in pointy brackets, the compiler will look for the file in whatever directories the project file has set to be places to look for include files ? typically the standard location for include files is the INCLUDE subdirectory of the directory where the compiler lives, but sometimes you will want to add other directories to the standard search path. Note that the compiler is not sensitive to the case of the letters used in the names of the include files.

The #define directive

The #define directive has this appearance.

#define ANYSTRING Any other string 

The string between the first two blank spaces after the #define is replaced everywhere by the string which fills out the rest of the line after the second blank space. Who does the replacing? The preprocessor. It effectively does a search and replace, replacing each 'ANYSTRING' by 'Any other string.' It's good programming practice, but not formally necessary, to always use all capital letters for a quantity which you #define.

Another, non-obvious effect of a #define statement is that it adds the first string to a special 'symbol table' that the preprocessor constructs for the code being compiled; the symbol table being a private list of what strings have been used in a #define.

#ifdef and related directives

The next group of preprocessor directives have to do with control flow. Lines of the form #ifdef, #ifndef, #else, and #endif conditionally include or exclude parts of the file depending on whether some symbol has been placed into the preprocessor symbol table by a #define.

#ifdef, #ifndef, #else, and #endif are preprocessor directives to the compiler. The first two stand, respectively, for 'if defined' and 'if not defined.' If the expression (or 'token') after the #ifdef has been defined with a #define, then the block of code up until the #endif is included; if the token is not defined, then the code is not included. If the token after an #ifndef has been defined with a #define, then the block of code up until the #endif is not included; if the token is not defined, then the code is included.

As mentioned above, you can #define something without putting a 'replacement string' for it. That is, we can have this line.

#define ANYHEADER_H 

This use of the #define directive adds the indicated string of letters to the preprocessor symbol table so as to possibly affect #ifdef or #endif statements.

There is also a defined() operator one can use in conjunction with a plain #if directive. So instead of #ifdef WIN_H, for instance, you can write #if defined(WIN_H), and instead of #ifndef WIN_H, you can write #if !defined(WIN_H).

The #pragma directive is used for miscellaneous kinds of special hints to the compiler. A common use is to turn off a warning that you don't care about. Thus the following line taken from our realnumber.h file turns off two warnings that result from treating a double as a float.

#pragma warning(disable: 4305 4244) 

Sometimes you don't want to bother using a pragma to turn off a warning message of the form, say, "information lost in conversion from double to int". You can tell the compiler that in a particular case you really don't mind rounding, say, sqrt(dx*dx + dy*dy) off to the closest integer. In C we would have done this with a 'cast' by putting (int) in front of the number. In C++ it's more common to write a cast as a 'constructor' by putting int(sqrt(dx*dx + dy*dy)). In general, if you can get rid of a warning message by adding a little bit of code it's worth doing, so that then you know that any messages that do pop up in your compiler are important.

The typedef convention

Although typedef isn't a preprocessor directive, it has much the same force. typedef gives us a mechanism for defining a synonym for an existing type. The syntax is as follows:

typedef existing-type synonym; 

If you want a synonym for a class name then its better to use typedef than define, as the compiler can then better check the consistency of what you're doing.

We might use a typedef in the case where a type name is very long and unwieldy. Thus we might do something like this (although actually we don't use this in the Pop Framework).

typedef CTypedPtrArray<CObArray, cCritter*> cCritterArray; 

Another case where we use typedef is when we want to try out different builds of our code. The Pop Framework has two possible typedef for the Real type in realnumber.h, and in vectortransformation.h, it has a typedef based on whether we plan to use 2D or 3D graphics.

    typedef class cVector2 cVector; 
    typedef class cMatrix2 cMatrix; 
    typedef class cVector3 cVector; 
    typedef class cMatrix3 cMatrix; 

    Part I: Software Engineering and Computer Games
    Part II: Software Engineering and Computer Games Reference