The Preprocessor
Understand preprocessor directives and their uses.
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!" << '\n';
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!" << '\n';
return 0;
}
After preprocessing (conceptually):
// Entire contents of iostream header inserted here
// (thousands of lines of declarations and definitions)
int main()
{
std::cout << "Hello!" << '\n';
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 << '\n';
std::cout << "Max array size: " << MAX_SIZE << '\n';
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 << '\n';
std::cout << "Maximum students: " << MAX_STUDENTS << '\n';
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) << '\n';
std::cout << "CUBE(3) = " << CUBE(3) << '\n';
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..." << '\n';
#ifdef DEBUG_MODE
std::cout << "DEBUG: This is debug information" << '\n';
std::cout << "DEBUG: Program initialized successfully" << '\n';
#endif
// Main program logic
int result{42};
std::cout << "Final result: " << result << '\n';
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" << '\n';
#elif PLATFORM == 2
std::cout << "Linux" << '\n';
#elif PLATFORM == 3
std::cout << "macOS" << '\n';
#else
std::cout << "Unknown platform" << '\n';
#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..." << '\n';
#ifdef ENABLE_LOGGING
std::cout << "Logging system enabled" << '\n';
#endif
#ifdef ENABLE_NETWORKING
std::cout << "Networking enabled on port " << SERVER_PORT << '\n';
std::cout << "Max connections: " << MAX_CONNECTIONS << '\n';
#endif
#ifdef FEATURE_USER_ACCOUNTS
std::cout << "User account system enabled" << '\n';
#endif
#ifdef FEATURE_PREMIUM_CONTENT
std::cout << "Premium content system enabled" << '\n';
#else
std::cout << "Premium content system disabled" << '\n';
#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" << '\n';
#ifdef DEBUG_BUILD
std::cout << "[DEBUG] Debug mode is active" << '\n';
std::cout << "[DEBUG] Extra debugging information" << '\n';
#endif
double result{10.0 / 2.0};
std::cout << "Result: " << result << '\n';
#ifdef DEBUG_BUILD
std::cout << "[DEBUG] Calculation completed" << '\n';
#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__ << '\n';
std::cout << "Line: " << __LINE__ << '\n';
std::cout << "Function: " << __func__ << '\n';
std::cout << "Date compiled: " << __DATE__ << '\n';
std::cout << "Time compiled: " << __TIME__ << '\n';
#ifdef __cplusplus
std::cout << "C++ standard version: " << __cplusplus << '\n';
#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
double also_wrong{MAX_STUDENTS * PI}; // Also compiles but types make no sense
std::cout << "Area: " << area << '\n'; // Area: 78.5397
std::cout << "Wrong calculation: " << wrong << '\n'; // Wrong calculation: 33.1416
return 0;
}
Multiple evaluation
#include <iostream>
#define SQUARE(x) ((x) * (x))
int getValue()
{
std::cout << "getValue() called" << '\n';
return 5;
}
int main()
{
// This calls getValue() twice!
int result{SQUARE(getValue())}; // Expands to: ((getValue()) * (getValue()))
std::cout << "Result: " << result << '\n';
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 << '\n';
}
int main()
{
std::cout << "Size in main: " << SIZE << '\n';
{
// Even in this inner block, SIZE is still 10
// Unlike variables, macros don't respect scope
std::cout << "Size in inner block: " << SIZE << '\n';
}
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 << '\n';
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.
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
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
No comments yet. Be the first to share your thoughts!