
Header files
The most common use of the preprocessor is the #include directive, intended to include header files in the source code. Header files contain definitions for functions, classes, and so on:
// file: main.cpp
#include <iostream>
#include "rect.h"
int main() {
Rect r(3.1, 4.05)
std::cout << r.get_area() << std::endl;
}
Let's suppose the header file rect.h is defined as follows:
// file: rect.h
struct Rect
{
private:
double side1_;
double side2_;
public:
Rect(double s1, double s2);
const double get_area() const;
};
The implementation is contained in rect.cpp:
// file: rect.cpp
#include "rect.h"
Rect::Rect(double s1, double s2)
: side1_(s1), side2_(s2)
{}
const double Rect::get_area() const {
return side1_ * side2_;
}
After the preprocessor examines main.cpp and rect.cpp, it replaces the #include directives with corresponding contents of iostream and rect.h for main.cpp and rect.h for rect.cpp. C++17 introduces the __has_include preprocessor constant expression. __has_include evaluates to 1 if the file with the specified name is found and 0 if not:
#if __has_include("custom_io_stream.h")
#include "custom_io_stream.h"
#else
#include <iostream>
#endif
When declaring header files, it's strongly advised to use so-called include-guards (#ifndef, #define, #endif) to avoid double declaration errors. We are going to introduce the technique shortly. Those are, again, preprocessor directives that allow us to avoid the following scenario: type Square is defined in square.h, which includes rect.h in order to derive Square from Rect:
// file: square.h
#include "rect.h"
struct Square : Rect {
Square(double s);
};
Including both square.h and rect.h in main.cpp leads to including rect.h twice:
// file: main.cpp
#include <iostream>
#include "rect.h"
#include "square.h"
/*
preprocessor replaces the following with the contents of square.h
*/
// code omitted for brevity
After preprocessing, the compiler will receive main.cpp in the following form:
// contents of the iostream file omitted for brevity
struct Rect {
// code omitted for brevity
};
struct Rect {
// code omitted for brevity
};
struct Square : Rect {
// code omitted for brevity
};
int main() {
// code omitted for brevity
}
The compiler will then produce an error because it encounters two declarations of type Rect. A header file should be guarded against multiple inclusions by using include-guards in the following way:
#ifndef RECT_H
#define RECT_H
struct Rect { ... }; // code omitted for brevity
#endif // RECT_H
When the preprocessor meets the header for the first time, RECT_H is not defined and everything between #ifndef and #endif will be processed accordingly, including the RECT_H definition. The second time the preprocessor includes the same header file in the same source file, it will omit the contents because RECT_H has already been defined.
These include-guards are part of directives that control the compilation of parts of the source file. All of the conditional compilation directives are #if, #ifdef, #ifndef, #else, #elif, and #endif.
Conditional compilation is useful in many cases; one of them is logging function calls in so-called debug mode. Before releasing the program, it is advised to debug your program and test against logical flaws. You might want to see what happens in the code after invoking a certain function, for example:
void foo() {
log("foo() called");
// do some useful job
}
void start() {
log("start() called");
foo();
// do some useful job
}
Each function calls the log() function, which is implemented as follows:
void log(const std::string& msg) {
#if DEBUG
std::cout << msg << std::endl;
#endif
}
The log() function will print the msg if DEBUG is defined. If you compile the project enabling DEBUG (using compiler flags, such as -D in g++), then the log() function will print the string passed to it; otherwise, it will do nothing.