Coming Soon
This lesson is currently being developed
Introduction to the preprocessor
Understand preprocessor directives and their uses.
What to Expect
Comprehensive explanations with practical examples
Interactive coding exercises to practice concepts
Knowledge quiz to test your understanding
Step-by-step guidance for beginners
Development Status
Content is being carefully crafted to provide the best learning experience
Preview
Early Preview Content
This content is still being developed and may change before publication.
2.10 — Introduction to the preprocessor
In this lesson, you'll learn about the C++ preprocessor, understand how it processes your code before compilation, and discover the most important preprocessor directives that every C++ programmer should know.
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 ──► Assembly ──► Linking ──► Executable
(.cpp) (expanded) (.o/.obj) (machine (final
code) 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
#include "myheader.h" // Include user-defined 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
#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 SQUARE(x) ((x) * (x))
int main()
{
double radius = 5.0;
double area = PI * SQUARE(radius); // Becomes: 3.14159 * ((radius) * (radius))
std::cout << "Area: " << area << std::endl;
return 0;
}
Output:
Area: 78.5398
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 MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define SQUARE(x) ((x) * (x))
#define CUBE(x) ((x) * (x) * (x))
int main()
{
int x = 5, y = 8;
std::cout << "MAX(5, 8) = " << MAX(x, y) << std::endl;
std::cout << "MIN(5, 8) = " << MIN(x, y) << std::endl;
std::cout << "SQUARE(7) = " << SQUARE(7) << std::endl;
std::cout << "CUBE(3) = " << CUBE(3) << std::endl;
return 0;
}
Output:
MAX(5, 8) = 8
MIN(5, 8) = 5
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;
#ifdef DEBUG_MODE
std::cout << "DEBUG: Result calculated: " << result << std::endl;
#endif
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
DEBUG: Result calculated: 42
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 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>
// Typically defined by the build system
// #define DEBUG_BUILD
#define RELEASE_BUILD
#ifdef DEBUG_BUILD
#define LOG(message) std::cout << "[DEBUG] " << message << std::endl
#define ASSERT(condition) if (!(condition)) { \
std::cout << "ASSERTION FAILED: " << #condition << std::endl; \
exit(1); }
#else
#define LOG(message) // Do nothing in release
#define ASSERT(condition) // Do nothing in release
#endif
double divide(double a, double b)
{
ASSERT(b != 0); // Check for division by zero in debug builds
LOG("Performing division: " + std::to_string(a) + " / " + std::to_string(b));
return a / b;
}
int main()
{
LOG("Program started");
double result = divide(10.0, 2.0);
std::cout << "Result: " << result << std::endl;
LOG("Program finished");
return 0;
}
Output (Debug build):
[DEBUG] Program started
[DEBUG] Performing division: 10.000000 / 2.000000
Result: 5
[DEBUG] Program finished
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: Dec 1 2023
Time compiled: 14:30:25
C++ standard version: 201703
Dangers and limitations of macros
Problem 1: No type checking
#include <iostream>
#define SQUARE(x) ((x) * (x))
int main()
{
int result = SQUARE(5); // Fine
std::cout << result << std::endl; // 25
// But this compiles too, even though it doesn't make sense:
std::string text = "hello";
// auto bad_result = SQUARE(text); // Would cause compilation error
return 0;
}
Problem 2: 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
Problem 3: Scope issues
#include <iostream>
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int main()
{
int a = 5;
{
int b = 10;
int result = MAX(a, b); // Works fine
std::cout << result << std::endl;
}
// The macro doesn't respect scope like functions do
return 0;
}
Modern alternatives to macros
For most cases, modern C++ provides better alternatives:
#include <iostream>
// Instead of #define constants:
// #define PI 3.14159
const double PI = 3.14159; // Better: type-safe, scoped
// Instead of function-like macros:
// #define SQUARE(x) ((x) * (x))
constexpr double square(double x) // Better: type-safe, no multiple evaluation
{
return x * x;
}
int main()
{
double area = PI * square(5.0);
std::cout << "Area: " << area << std::endl;
return 0;
}
Best practices for preprocessor usage
✅ Good uses of the preprocessor:
// 1. File inclusion
#include <iostream>
#include "myheader.h"
// 2. Conditional compilation for different platforms
#ifdef _WIN32
// Windows-specific code
#elif defined(__linux__)
// Linux-specific code
#endif
// 3. Debug/release builds
#ifdef DEBUG
#define DBG_PRINT(x) std::cout << x << std::endl
#else
#define DBG_PRINT(x)
#endif
// 4. API configuration
#define LIBRARY_VERSION_MAJOR 2
#define LIBRARY_VERSION_MINOR 1
❌ Avoid these patterns:
// Don't use macros for constants (use const/constexpr instead)
#define MAX_SIZE 100 // Bad
// Don't use complex function-like macros (use functions instead)
#define COMPLEX_CALC(a, b, c) ((a) + (b) * (c) - sqrt((a) * (b))) // Bad
// Don't use macros that look like functions but aren't
#define increment(x) ++(x) // Confusing
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 make code harder to debug
- Can create hard-to-understand bugs
Modern alternatives:
- Use
const
/constexpr
instead of#define
for constants - Use
inline
functions instead of function-like macros - Use
if constexpr
for conditional code (C++17+)
The preprocessor is essential for understanding how C++ programs are built, but use it judiciously in your own code.
Quiz
- What happens during the preprocessing stage?
- What's the difference between
#include <file>
and#include "file"
? - What problems can function-like macros cause?
- When would you use conditional compilation?
- What are some modern alternatives to preprocessor macros?
Practice exercises
Try these exercises to understand the preprocessor:
- Configuration system: Create a program that uses
#define
to configure different features (logging, debugging, etc.) - Platform detection: Write code that compiles differently for different operating systems
- Debug macros: Create useful debug macros that are disabled in release builds
- Macro problems: Write examples showing the problems with function-like macros and fix them using modern C++ alternatives
Explore More Courses
Discover other available courses while this lesson is being prepared.
Browse CoursesLesson Discussion
Share your thoughts and questions