Use Conditional Compilation to Hide Platform Dependencies and Implement #include Guards

he C++ preprocessor may seem like a prehistoric relic that has no place in state-of-the-art C++ projects, but this isn’t really so. Although every beginner nowadays knows how to use inline functions and const variables instead of #define macros, the preprocessor is still an indispensable tool when it comes to source file management and cross-platform development.


How can you avoid multiple inclusions of the same header file during a compilation session? How can you overcome platform-dependencies in a large-scale project?


Use the preprocessor to perform conditional compilation.

Demonstrating the Problem
Suppose you’re developing an application that runs on both 32- and 64-bit environments. The differences between these two platforms aren’t trivial: the representation of pointers under each platform is different as are the alignment requirements, and the sizes of built-in types. However, the project is essentially the same on both platforms so you don’t want to maintain two distinct versions of each source file. In addition, you need to organize your project in an easy to maintain set of self-contained units. The preprocessor can simplify these tasks.

#include Guards
Even if you’re not planning for cross-platform development, every non-trivial project consists of one or more units. A unit is a pair of .cpp and .h files that each contain a logical entity such as a class, a function, a template, an enum type, etc. Usually, the .cpp file contains the implementation, whereas the .h file contains only the declarations. The following Coord class exemplifies this. Its declaration appears in a header file and its members are defined in a matching .cpp file:

// file Coord.hclass Coord{private: int _x; int _y; static int counter;public: explicit Coord (int x=0, int y=0); int getX () const; int getY () const;};// file Coord..cpp#include "Coord.h"Coord::Coord(int x, int y): _x(x), _y(y){}int Coord::getX () const{  return _x; }int Coord::getY () const{  return _y;}int Coord::counter=0; //define the static member

The first problem in this code is that Coord.h might be #included multiple times during the same compilation session. To avert this, use an “#include guard. An #include guard is essentially a macro flag. When it isn’t set, this means that the header is being #included for the first time and should therefore be visible to the compiler. Otherwise, the header’s content is hidden from the compiler by macro magic. An #include guard should appear at the first line of every header file:

// file Coord.h#if !defined (COORD_H)//#included for the first time? #define COORD_H //then set the flag and make the rest                 //of this header visible to the compilerclass Coord {private: int _x; int _y;public: explicit Coord (int x=0, int y=0); int getX () const; int getY () const;};#endif //COORD_H

Don’t forget to add a matching #endif at the bottom of the file. Notice that you don’t have to specify a meaningful value for the flag. The flag’s name usually consists of the header’s name and _H attached to it. Of course, you may choose any other convention that ensures uniqueness and clarity. From now on, Coord.h will be #included at most once in every compilation session.

Note: All standard headers already have #include guards in them so you don’t need to add them manually to these headers.

Conditional Compilation and Cross-platform Development
Every seasoned programmer is familiar with compiler-dependent quirks. For instance, a certain compiler treats char as an unsigned type, whereas another compiler treats it as a signed type. Similarly, long can be a 32-bit integer on one machine whereas on a 64-bit architecture it may be a 64-bit integer. To minimize the effects of such dependencies, use conditional compilation.

During compilation, each compiler and operating system create predefined macros that identify various properties of the project’s configuration. For example, if the project is configured for debug mode, the macro _DEBUG is automatically #defined. Similarly, you can define a platform-independent 32-bit integer like this:

#if defined (_IA64) //is it a 64-bit platform? typedef int INT32; //a 32-bit integer#elif defined (_IA32) //else if typedef long INT32;#else typedef long INT32; //default #endif // defined (_IA64)  

This set of conditions automatically declares the proper typedef for every environment. Consequently, the typedef name INT32 will refer to the correct datatype at compile time. The #elif directive standard for ‘else if’, whereas plain #else should be the last default case.

Necessity Is the Mother of All Macros
Many programming languages disposed of a preprocessor, believing they could offer more advanced facilities of source file configuration and portability. However, since C++ uses header files extensively and because it’s used in diverse environments, conditional compilation is still an indispensable tool in every programmer’s arsenal.

Share the Post:
Share on facebook
Share on twitter
Share on linkedin

Overview

Recent Articles: