Skip to content

C++ Preprocessor

The C++ preprocessor is a program that processes the source code before it is passed to the actual compiler. It operates on directives, which are lines starting with #. These directives instruct the preprocessor to perform actions like including files, defining macros, and conditionally compiling code.

Preprocessor directives are not C++ statements; they do not end with a semicolon (;). Only whitespace characters can precede a directive on a line.

Common preprocessor directives include #include, #define, #ifdef, #ifndef, #if, #else, #elif, #endif, #pragma.

This is the most common directive. It tells the preprocessor to insert the contents of another file (usually a header file containing declarations) into the current source file at that point.

  • #include <filename>: Searches for the file in standard system directories (e.g., standard library headers like <iostream>, <vector>).
  • #include "filename": Searches first in the directory containing the current source file, then in standard system directories (typically used for your own project headers).

The #define directive creates macros. Macros perform text substitution before compilation.

1. Object-like Macros (Symbolic Constants):

#define identifier replacement_text

The preprocessor replaces every subsequent occurrence of identifier with replacement_text.

#include <iostream>
#define PI 3.14159
#define GREETING "Hello, World!"
int main() {
double radius = 5.0;
double area = PI * radius * radius;
std::cout << GREETING << std::endl;
std::cout << "Value of PI: " << PI << std::endl;
std::cout << "Area: " << area << std::endl;
return 0;
}
// Preprocessor output (conceptually):
// int main() {
// double radius = 5.0;
// double area = 3.14159 * radius * radius;
// std::cout << "Hello, World!" << std::endl;
// std::cout << "Value of PI: " << 3.14159 << std::endl;
// std::cout << "Area: " << area << std::endl;
// return 0;
// }

Modern C++ Alternative: For constants, prefer const or constexpr variables. They provide type safety and respect scope, unlike macros.

const double PI_CONST = 3.14159;
constexpr int MAX_USERS = 100; // Compile-time constant

2. Function-like Macros:

#define MACRO_NAME(param1, param2, ...) replacement_text

These macros take arguments and perform text substitution based on them. Use with extreme caution! They are prone to errors due to unexpected expansion and lack of type checking.

#include <iostream>
// Potential pitfalls: requires careful parenthesizing!
#define MIN(a, b) (((a) < (b)) ? (a) : (b))
int main() {
int x = 100;
int y = 30;
std::cout << "The minimum is " << MIN(x, y) << std::endl; // Expands to (((x) < (y)) ? (x) : (y))
// Example of potential error without parentheses in definition:
// #define MULTIPLY(a, b) a * b
// int result = MULTIPLY(2 + 3, 4); // Expands to 2 + 3 * 4 = 14, NOT (2+3)*4 = 20
return 0;
}

Modern C++ Alternative: For function-like behavior, prefer inline functions or function templates. They offer type safety, scoping, and are generally easier to debug.

template <typename T>
inline T findMin(const T& a, const T& b) { // Inline function template
return (a < b) ? a : b;
}
int main() {
std::cout << "Min using template: " << findMin(100, 30) << std::endl;
std::cout << "Min using template: " << findMin(5.5, 2.1) << std::endl;
}

Preprocessor directives allow you to selectively include or exclude blocks of code from compilation based on certain conditions. This is useful for platform-specific code, debugging builds, or enabling/disabling features.

  • #ifdef identifier: Includes the following code if identifier has been defined (e.g., via #define).
  • #ifndef identifier: Includes the following code if identifier has not been defined.
  • #if expression: Includes the following code if the constant expression evaluates to non-zero (true).
  • #else: Included if the preceding #if, #ifdef, #ifndef, or #elif condition was false.
  • #elif expression: An “else if” condition, checked if the preceding #if/#elif was false.
  • #endif: Marks the end of a conditional block (#if, #ifdef, #ifndef).

Example: Debugging Messages

#include <iostream>
// Define DEBUG macro (perhaps via compiler flag -DDEBUG)
#define DEBUG
int main() {
int x = 5;
#ifdef DEBUG
std::cerr << "Debug: Starting main function." << std::endl;
std::cerr << "Debug: Variable x = " << x << std::endl;
#endif
std::cout << "Performing main task..." << std::endl;
#if defined(DEBUG) && DEBUG > 1 // Check if DEBUG is defined and greater than 1
std::cerr << "Debug: Verbose logging enabled." << std::endl;
#endif
#ifndef NDEBUG // NDEBUG is often defined for release builds
std::cout << "Assertion checks might be enabled." << std::endl;
#else
std::cout << "Running in release mode (NDEBUG defined)." << std::endl;
#endif
#ifdef DEBUG
std::cerr << "Debug: Exiting main function." << std::endl;
#endif
return 0;
}

Common use case: Include guards in header files to prevent multiple inclusions:

my_header.h
#ifndef MY_HEADER_H
#define MY_HEADER_H
// Header content (declarations, etc.)
class MyClass { /* ... */ };
#endif // MY_HEADER_H

Alternatively, many modern compilers support #pragma once, which achieves the same goal with simpler syntax, though it’s not part of the official standard.

my_header_pragma.h
#pragma once
// Header content
class MyClass2 { /* ... */ };

These operators are used within #define directives:

  • # (Stringizing Operator): Converts a macro parameter into a string literal.
  • ## (Token-Pasting Operator): Concatenates two tokens (identifiers, keywords, etc.) into a single token.

Example:

#include <iostream>
#include <string>
#define STRINGIZE(x) #x
#define CONCAT(a, b) a##b
int main() {
// Using Stringize
std::cout << STRINGIZE(Hello C++) << std::endl; // Expands to: std::cout << "Hello C++" << std::endl;
const char* varNameStr = STRINGIZE(myVariable);
std::cout << "Variable name: " << varNameStr << std::endl; // Outputs: Variable name: myVariable
// Using Concatenate
int xy = 100;
std::cout << CONCAT(x, y) << std::endl; // Expands to: std::cout << xy << std::endl; Outputs: 100
std::string CONCAT(user, Name) = "Alice"; // Creates variable userName
std::cout << "User: " << userName << std::endl; // Outputs: User: Alice
return 0;
}

While occasionally useful, these operators can make code obscure and are less common in modern C++ development compared to alternatives like templates or constexpr functions.

C++ defines several standard predefined macros that provide useful information:

MacroDescription
__LINE__Integer literal: The current line number in the source file.
__FILE__String literal: The name of the current source file.
__DATE__String literal: The date the source file was compiled (e.g., “MMM DD YYYY”).
__TIME__String literal: The time the source file was compiled (e.g., “HH:MM:SS”).
__cplusplusInteger literal: The version of the C++ standard being used (e.g., 201103L for C++11, 201402L for C++14, 201703L for C++17, 202002L for C++20).
__func__ (Not strictly a macro, but similar)String literal: The name of the current function (available inside functions).

Example:

#include <iostream>
#include <string>
void log_message(const std::string& msg) {
std::cout << "[File: " << __FILE__
<< ", Line: " << __LINE__
<< ", Func: " << __func__ // Use __func__ inside function
<< "] " << msg << std::endl;
}
int main() {
std::cout << "Compiling using C++ version: " << __cplusplus << std::endl;
std::cout << "Compiled on: " << __DATE__ << " at " << __TIME__ << std::endl;
log_message("Entering main function.");
return 0;
}

Output will vary based on compiler, time, and standard version used.