What is the preprocessor?

The preprocessor is a program that processes your source code before the compiler sees it. It handles directives (commands that start with #) and performs text substitution and file inclusion.

The compilation process actually has several stages:

Source Code ──► Preprocessing ──► Compilation ────► Assembler ────► Linking ────► Executable
   (.cpp)         (expanded)        (.s/.asm)       (.o/.obj)    (final program)

The preprocessor runs during the preprocessing stage and:

  • Includes header files (#include)
  • Expands macros (#define)
  • Handles conditional compilation (#if, #ifdef)
  • Removes comments
  • Processes other preprocessor directives
Note: This process of preprocessing, compiling, and linking is called translation. The preprocessor is the first step, modifying your source code by processing directives (lines starting with #). The compiler then works on this modified code.

The #include directive

You've already been using the most common preprocessor directive:

#include <iostream>  // Include system header

int main()
{
    std::cout << "Hello, World!" << std::endl;
    return 0;
}

How #include works

When the preprocessor encounters #include, it literally copies the entire contents of the specified file and pastes it at that location:

Before preprocessing:

#include <iostream>

int main()
{
    std::cout << "Hello!" << std::endl;
    return 0;
}

After preprocessing (conceptually):

// Entire contents of iostream header inserted here
// (thousands of lines of declarations and definitions)

int main()
{
    std::cout << "Hello!" << std::endl;
    return 0;
}

Angle brackets vs quotes

In the upcoming lessons you will define your own header files, user defined headers are declare differently to system defined headers.

#include <iostream>     // System headers - search system directories first
#include "myfile.h"     // User headers - search current directory first

The #define directive

#define creates macros - simple text substitutions:

#include <iostream>

#define PI 3.14159
#define MAX_SIZE 100

int main()
{
    double radius = 5.0;
    double area = PI * radius * radius;  // PI becomes 3.14159

    std::cout << "Area: " << area << std::endl;
    std::cout << "Max array size: " << MAX_SIZE << std::endl;

    return 0;
}

Output:

Area: 78.5398
Max array size: 100

Object-like macros (constants)

Avoid in modern C++: Object-like macros should be avoided in modern C++. In the upcoming lesson we will show you how to use `const` or `constexpr` variables instead - they provide type safety, scope rules, and better debugging support.
#include <iostream>

#define MAX_STUDENTS 100
#define PROGRAM_NAME "Student Management System"
#define VERSION 2.1

int main()
{
    std::cout << PROGRAM_NAME << " v" << VERSION << std::endl;
    std::cout << "Maximum students: " << MAX_STUDENTS << std::endl;

    return 0;
}

Output:

Student Management System v2.1
Maximum students: 100

Function-like macros

Avoid in modern C++: Function-like macros are not considered safe and should be avoided in modern C++. Just use a normal function!
#include <iostream>

#define SQUARE(x) ((x) * (x))
#define CUBE(x) ((x) * (x) * (x))

int main()
{
    std::cout << "SQUARE(7) = " << SQUARE(7) << std::endl;
    std::cout << "CUBE(3) = " << CUBE(3) << std::endl;

    return 0;
}

Output:

SQUARE(7) = 49
CUBE(3) = 27

Conditional compilation

Preprocessor directives allow you to include or exclude code based on conditions:

#ifdef and #ifndef

#include <iostream>

#define DEBUG_MODE  // Comment this out to disable debug output

int main()
{
    std::cout << "Program starting..." << std::endl;

#ifdef DEBUG_MODE
    std::cout << "DEBUG: This is debug information" << std::endl;
    std::cout << "DEBUG: Program initialized successfully" << std::endl;
#endif

    // Main program logic
    int result = 42;

    std::cout << "Final result: " << result << std::endl;

    return 0;
}

Output (with DEBUG_MODE defined):

Program starting...
DEBUG: This is debug information
DEBUG: Program initialized successfully
Final result: 42

Output (without DEBUG_MODE defined):

Program starting...
Final result: 42

#if, #elif, #else

#include <iostream>

#define PLATFORM 2  // 1 = Windows, 2 = Linux, 3 = macOS

int main()
{
    std::cout << "Compiling for: ";

#if PLATFORM == 1
    std::cout << "Windows" << std::endl;
#elif PLATFORM == 2
    std::cout << "Linux" << std::endl;
#elif PLATFORM == 3
    std::cout << "macOS" << std::endl;
#else
    std::cout << "Unknown platform" << std::endl;
#endif

    return 0;
}

Output:

Compiling for: Linux

Practical (but legacy) example: Configuration system

This is an example for demonstration purposes, there are better ways to do this.
#include <iostream>

// Configuration defines
#define ENABLE_LOGGING
#define ENABLE_NETWORKING
#define MAX_CONNECTIONS 10
#define SERVER_PORT 8080

// Feature toggles
#define FEATURE_USER_ACCOUNTS
// #define FEATURE_PREMIUM_CONTENT  // Commented out = disabled

void initializeSystem()
{
    std::cout << "Initializing system..." << std::endl;

#ifdef ENABLE_LOGGING
    std::cout << "Logging system enabled" << std::endl;
#endif

#ifdef ENABLE_NETWORKING
    std::cout << "Networking enabled on port " << SERVER_PORT << std::endl;
    std::cout << "Max connections: " << MAX_CONNECTIONS << std::endl;
#endif

#ifdef FEATURE_USER_ACCOUNTS
    std::cout << "User account system enabled" << std::endl;
#endif

#ifdef FEATURE_PREMIUM_CONTENT
    std::cout << "Premium content system enabled" << std::endl;
#else
    std::cout << "Premium content system disabled" << std::endl;
#endif
}

int main()
{
    initializeSystem();
    return 0;
}

Output:

Initializing system...
Logging system enabled
Networking enabled on port 8080
Max connections: 10
User account system enabled
Premium content system disabled

Debug vs Release builds

A common use of the preprocessor is to create different behavior for debug and release builds:

#include <iostream>

#define DEBUG_BUILD  // Comment this line out for release build

int main()
{
    std::cout << "Program started" << std::endl;

#ifdef DEBUG_BUILD
    std::cout << "[DEBUG] Debug mode is active" << std::endl;
    std::cout << "[DEBUG] Extra debugging information" << std::endl;
#endif

    double result = 10.0 / 2.0;
    std::cout << "Result: " << result << std::endl;

#ifdef DEBUG_BUILD
    std::cout << "[DEBUG] Calculation completed" << std::endl;
#endif

    return 0;
}

Output (Debug build):

Program started
[DEBUG] Debug mode is active
[DEBUG] Extra debugging information
Result: 5
[DEBUG] Calculation completed

Output (Release build):

Result: 5

Common built-in macros

C++ provides several predefined macros:

#include <iostream>

int main()
{
    std::cout << "File: " << __FILE__ << std::endl;
    std::cout << "Line: " << __LINE__ << std::endl;
    std::cout << "Function: " << __func__ << std::endl;
    std::cout << "Date compiled: " << __DATE__ << std::endl;
    std::cout << "Time compiled: " << __TIME__ << std::endl;

#ifdef __cplusplus
    std::cout << "C++ standard version: " << __cplusplus << std::endl;
#endif

    return 0;
}

Output (example):

File: main.cpp
Line: 6
Function: main
Date compiled: Sep 26 2043 (the current date)
Time compiled: 14:30:25 (the current time)
C++ standard version: 202002

Dangers and limitations of macros

No type checking

#include <iostream>

#define MAX_STUDENTS 30
#define PI 3.14159

int main()
{
    int students = MAX_STUDENTS;     // Works fine - MAX_STUDENTS is 30
    double area = PI * 5 * 5;        // Works fine - PI is 3.14159

    // Macros don't provide type safety like variables do
    // If we accidentally mix them up:
    double wrong = PI + MAX_STUDENTS;   // Compiles but probably not intended
    int also_wrong = MAX_STUDENTS * PI; // Also compiles but types make no sense

    std::cout << "Area: " << area << std::endl; // Area: 78.5397
    std::cout << "Wrong calculation: " << wrong << std::endl; // Wrong calculation: 33.1416

    return 0;
}

Multiple evaluation

#include <iostream>

#define SQUARE(x) ((x) * (x))

int getValue()
{
    std::cout << "getValue() called" << std::endl;
    return 5;
}

int main()
{
    // This calls getValue() twice!
    int result = SQUARE(getValue());  // Expands to: ((getValue()) * (getValue()))
    std::cout << "Result: " << result << std::endl;

    return 0;
}

Output:

getValue() called
getValue() called
Result: 25

No scope rules

#include <iostream>

#define SIZE 10

void someFunction()
{
    // SIZE is still 10 here - macros ignore scope boundaries
    std::cout << "Size in function: " << SIZE << std::endl;
}

int main()
{
    std::cout << "Size in main: " << SIZE << std::endl;

    {
        // Even in this inner block, SIZE is still 10
        // Unlike variables, macros don't respect scope
        std::cout << "Size in inner block: " << SIZE << std::endl;
    }

    someFunction();

    return 0;
}

Modern alternatives to macros

For most cases, modern C++ provides better alternatives:

#include <iostream>

// Note: we will do const in future lessons
// Instead of #define constants:
// #define PI 3.14159
const double PI = 3.14159;  // Better: type-safe

namespace Math {
    const double PI = 3.14159;  // Better: type-safe, scoped
}

// Instead of function-like macros:
// #define SQUARE(x) ((x) * (x))
double square(double x)  // Better: type-safe, no multiple evaluation
{
    return x * x;
}

int main()
{
    double area = Math::PI * square(5.0);
    std::cout << "Area: " << area << std::endl;

    return 0;
}

Summary

The preprocessor is a powerful but dangerous tool in C++:

Key concepts:

  • Preprocessor: Processes code before compilation
  • Directives: Commands starting with #
  • Text substitution: Literal replacement of text
  • Conditional compilation: Include/exclude code based on conditions

Common directives:

  • #include: File inclusion
  • #define: Macro definition
  • #ifdef/#ifndef: Conditional compilation
  • #if/#elif/#else: Multi-way conditional compilation

Benefits:

  • File inclusion: Reuse code across files
  • Conditional compilation: Different builds for different platforms/configurations
  • Configuration: Enable/disable features at compile time

Dangers:

  • No type checking
  • Multiple evaluation of arguments
  • Can create hard to understand bugs

Modern alternatives:

  • Use const variables instead of #define for constants (we'll cover this in a future lesson)
  • Use regular functions instead of function-like macros
  • Use regular if statements for most conditional code

The preprocessor is essential for understanding how C++ programs are built, but use it judiciously in your own code.