Ready to practice?
Sign up to access interactive coding exercises and track your progress.
The Preprocessor
Understand preprocessor directives and their uses.
Prerequisites
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
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)
#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
#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
#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
constvariables instead of#definefor constants (we'll cover this in a future lesson) - Use regular functions instead of function-like macros
- Use regular
ifstatements for most conditional code
The preprocessor is essential for understanding how C++ programs are built, but use it judiciously in your own code.
The Preprocessor - Quiz
Test your understanding of the lesson.
Practice Exercises
Understanding the Preprocessor
Practice using built-in macros and conditional compilation with the preprocessor.
Lesson Discussion
Share your thoughts and questions