C++ Preprocessor
C++ Preprocessor Directives
Section titled “C++ Preprocessor Directives”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.
The #include Directive
Section titled “The #include Directive”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: Macros
Section titled “The #define Directive: Macros”The #define directive creates macros. Macros perform text substitution before compilation.
1. Object-like Macros (Symbolic Constants):
#define identifier replacement_textThe 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 constant2. Function-like Macros:
#define MACRO_NAME(param1, param2, ...) replacement_textThese 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;}Conditional Compilation
Section titled “Conditional Compilation”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 ifidentifierhas been defined (e.g., via#define).#ifndef identifier: Includes the following code ifidentifierhas not been defined.#if expression: Includes the following code if the constantexpressionevaluates to non-zero (true).#else: Included if the preceding#if,#ifdef,#ifndef, or#elifcondition was false.#elif expression: An “else if” condition, checked if the preceding#if/#elifwas 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:
#ifndef MY_HEADER_H#define MY_HEADER_H
// Header content (declarations, etc.)class MyClass { /* ... */ };
#endif // MY_HEADER_HAlternatively, many modern compilers support #pragma once, which achieves the same goal with simpler syntax, though it’s not part of the official standard.
#pragma once
// Header contentclass MyClass2 { /* ... */ };The # and ## Operators (Macro Operators)
Section titled “The # and ## Operators (Macro Operators)”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.
Predefined Macros
Section titled “Predefined Macros”C++ defines several standard predefined macros that provide useful information:
| Macro | Description |
|---|---|
__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”). |
__cplusplus | Integer 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.