Preprocessor Directives in C++
The preprocessor is a program that processes C++ source code before it is compiled. Preprocessor directives start with the # symbol and are executed at compile time, not at runtime. The preprocessor performs text substitution, file merging, and conditional removal of code.
Some of the most commonly used preprocessor directives:
#include— Includes a header file from a library or another file.#define— Defines a macro (a constant or a macro with arguments).#ifdef/#ifndef/#else/#endif— Conditional compilation.#pragma— Compiler-specific instructions.#line— Changes the line number and file name for debugging.#error— Stops compilation with an error message.
An important difference between #define and constexpr: #define works at the text level (pure substitution), while constexpr works at the type level and has scope. Use constexpr instead of #define for constants because it is safer and type-aware.
Include guards (#ifndef/#define/#endif or #pragma once) prevent a header file from being included more than once, which can cause compilation errors.
Code Example
#include <iostream>
#include <string>
#include <vector>
using namespace std;
// ==================== #define Macros ====================
#define PI 3.14159265358979
#define MAX_SIZE 100
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define DEBUG 1
// ==================== Include Guards ====================
#ifndef MYHEADER_H
#define MYHEADER_H
// The header file contents go here
// It will not be included twice because of the include guard
#endif // MYHEADER_H
// ==================== Conditional Compilation ====================
#ifdef DEBUG
#define LOG(msg) cout << "[DEBUG] " << msg << endl
#else
#define LOG(msg) // Does nothing
#endif
#ifdef _WIN32
#define PLATFORM "Windows"
#elif defined(__linux__)
#define PLATFORM "Linux"
#elif defined(__APPLE__)
#define PLATFORM "macOS"
#else
#define PLATFORM "Unknown"
#endif
// ==================== #line Directive ====================
// Changes the line number for debugging
#line 100 "custom_file.cpp"
// ==================== #error Directive ====================
#ifndef __cplusplus
#error "This file must be compiled with a C++ compiler!"
#endif
// ==================== Macros with stringify ====================
#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)
#define CONCAT(a, b) a##b
// ==================== Utility Macros ====================
#define UNUSED(x) (void)(x)
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
// ==================== constexpr vs #define ====================
constexpr double PI_CONSTEXPR = 3.14159265358979;
constexpr int MAX_CONSTEXPR = 100;
int main() {
// ==================== Basic #define ====================
cout << "=== Dasar #define ===" << endl;
cout << "PI = " << PI << endl;
cout << "MAX_SIZE = " << MAX_SIZE << endl;
// Macros with arguments
int val = 5;
cout << "SQUARE(5) = " << SQUARE(val) << endl;
cout << "MAX(10, 20) = " << MAX(10, 20) << endl;
cout << "MIN(10, 20) = " << MIN(10, 20) << endl;
// Warning: macros without parentheses can be dangerous
#define BAD_SQUARE(x) x * x
cout << "BAD_SQUARE(3+2) = " << BAD_SQUARE(3 + 2) << endl; // 3+2*3+2 = 11 (not 25!)
// Always use parentheses: #define GOOD_SQUARE(x) ((x) * (x))
// ==================== Conditional Compilation ====================
cout << "\n=== Conditional Compilation ===" << endl;
LOG("Program started");
LOG("Variable x = " << val);
cout << "Platform: " << PLATFORM << endl;
#ifdef DEBUG
cout << "DEBUG mode active" << endl;
#else
cout << "Release mode" << endl;
#endif
// ==================== #ifdef vs #if defined() ====================
cout << "\n=== #ifdef vs #if defined() ===" << endl;
#ifdef DEBUG
cout << "DEBUG defined" << endl;
#endif
#if defined(DEBUG) && !defined(NDEBUG)
cout << "DEBUG defined AND NDEBUG not defined" << endl;
#endif
// ==================== Macro Operations ====================
cout << "\n=== Macro Operations ===" << endl;
// Stringify
cout << "STRINGIFY(Hello) = " << STRINGIFY(Hello) << endl;
cout << "TOSTRING(PI) = " << TOSTRING(PI) << endl;
// Token pasting
int var10 = 100;
cout << "CONCAT(var, 10) = " << CONCAT(var, 10) << endl;
// ==================== Debug Macros ====================
cout << "\n=== Debug Macros ===" << endl;
#define TRACE_VAR(x) cout << #x << " = " << x << endl
#define FUNCTION_ENTRY() cout << "Entering " << __func__ << endl
int x = 42;
string name = "John";
TRACE_VAR(x);
TRACE_VAR(name);
FUNCTION_ENTRY();
// ==================== Platform-Specific Code ====================
cout << "\n=== Platform-Specific ===" << endl;
#if defined(_WIN32)
cout << "Running on Windows" << endl;
#include <windows.h>
#elif defined(__linux__)
cout << "Running on Linux" << endl;
#include <unistd.h>
#elif defined(__APPLE__)
cout << "Running on macOS" << endl;
#endif
// ==================== constexpr vs #define ====================
cout << "\n=== constexpr vs #define ===" << endl;
cout << "#define PI = " << PI << endl;
cout << "constexpr PI = " << PI_CONSTEXPR << endl;
// constexpr has a data type, #define does not
// PI_CONSTEXPR is explicitly a double
// PI is pure text that is substituted
// constexpr can be used in templates
// #define cannot
// ==================== Array Size Macro ====================
cout << "\n=== Array Size Macro ===" << endl;
int arr[] = {1, 2, 3, 4, 5};
cout << "Array size: " << ARRAY_SIZE(arr) << endl;
// ==================== Predefined Macros ====================
cout << "\n=== Predefined Macros ===" << endl;
cout << "__cplusplus: " << __cplusplus << endl;
cout << "__FILE__: " << __FILE__ << endl;
cout << "__LINE__: " << __LINE__ << endl;
cout << "__func__: " << __func__ << endl;
cout << "__DATE__: " << __DATE__ << endl;
cout << "__TIME__: " << __TIME__ << endl;
// ==================== #pragma ====================
cout << "\n=== #pragma ===" << endl;
// #pragma once - prevent double inclusion (non-standard but widely supported)
// #pragma pack(1) - control struct padding
// #pragma warning(disable: 4100) - disable a specific warning (MSVC)
#pragma message("Compilation message: preprocessor directives processed")
// ==================== Complex Conditional ====================
cout << "\n=== Complex Conditional ===" << endl;
#define VERSION_MAJOR 2
#define VERSION_MINOR 1
#define VERSION_PATCH 0
#if VERSION_MAJOR > 1
#define IS_MODERN 1
#else
#define IS_MODERN 0
#endif
#if IS_MODERN
cout << "Version " << VERSION_MAJOR << "." << VERSION_MINOR << "." << VERSION_PATCH << " (Modern)" << endl;
#else
cout << "Version " << VERSION_MAJOR << "." << VERSION_MINOR << "." << VERSION_PATCH << " (Legacy)" << endl;
#endif
// ==================== Utility Macros ====================
cout << "\n=== Utility Macros ===" << endl;
int unusedVar = 10;
UNUSED(unusedVar); // Avoids an unused-variable warning
cout << "\n=== Best Practices ===" << endl;
cout << "1. Use constexpr instead of #define for constants." << endl;
cout << "2. Always use parentheses in macros: #define SQ(x) ((x)*(x))" << endl;
cout << "3. Use #ifndef/#define/#endif or #pragma once for include guards." << endl;
cout << "4. Use #ifdef DEBUG for debug-only code." << endl;
cout << "5. Avoid complex macros; use inline constexpr or templates." << endl;
return 0;
}
Code Explanation
-
#define PI 3.14159...— Defines a constant macro. The preprocessor will replace every occurrence ofPIwith the text3.14159...before compilation. -
#define SQUARE(x) ((x) * (x))— A macro with an argument. ALWAYS use parentheses around the parameter and the result to avoid operator-precedence bugs. Bad example:#define BAD(x) x*x→BAD(3+2)=3+2*3+2=11(not 25!). -
#ifndef MYHEADER_H/#define MYHEADER_H/#endif— An include guard. Prevents a header file from being included more than once, which can cause redefinition errors. -
#ifdef DEBUG— Conditional compilation. Compiles the code inside it only ifDEBUGhas been defined. -
#if defined(__APPLE__)— Conditional compilation based on the platform. Useful for code specific to a particular platform. -
#line 100 "custom_file.cpp"— Changes the line number and file name reported by the compiler on an error. Useful for debugging generated files. -
#error "message"— Stops compilation with an error message. Useful for ensuring certain conditions are met. -
#pragma message("...")— Displays a message during compilation.#pragma onceprevents double inclusion (non-standard but widely supported). -
#define STRINGIFY(x) #x— The stringify operator. Converts a token into a string.STRINGIFY(Hello)→"Hello". -
#define CONCAT(a, b) a##b— The token-pasting operator. Joins two tokens.CONCAT(var, 10)→var10. -
__cplusplus,__FILE__,__LINE__— Predefined macros provided by the compiler. Useful for debugging and logging. -
constexprvs#define—constexprhas a data type, has scope, and is safe in templates.#defineis pure text substitution with no type and no scope.
Everyday Use Cases
Imagine you are developing a cross-platform application (running on Windows, Linux, and macOS). Your code needs to behave differently on each platform:
// File: config.h
#ifndef CONFIG_H
#define CONFIG_H
#ifdef _WIN32
#define LINE_ENDING "\r\n"
#define PATH_SEPARATOR "\\"
#elif defined(__APPLE__) || defined(__linux__)
#define LINE_ENDING "\n"
#define PATH_SEPARATOR "/"
#endif
// Debug mode
#ifdef NDEBUG
#define DEBUG_LOG(msg) ((void)0)
#else
#define DEBUG_LOG(msg) std::cout << "[DEBUG] " << msg << std::endl
#endif
// Build info
#define BUILD_VERSION "1.2.3"
#define BUILD_DATE __DATE__ " " __TIME__
#endif
// File: main.cpp
#include "config.h"
int main() {
DEBUG_LOG("Program started");
cout << "Build: " << BUILD_VERSION << " (" << BUILD_DATE << ")" << endl;
// The code behaves differently on each platform
}
Conclusion
Preprocessor directives process code before compilation. #define creates macros, #include includes files, and #ifdef/#ifndef control conditional compilation. Use constexpr instead of #define for constants because it is safer and type-aware. Include guards prevent a header file from being included twice. Macros must be used carefully — always use parentheses and consider modern alternatives such as constexpr and inline.