Skillversitas

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

  1. #define PI 3.14159... — Defines a constant macro. The preprocessor will replace every occurrence of PI with the text 3.14159... before compilation.

  2. #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!).

  3. #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.

  4. #ifdef DEBUG — Conditional compilation. Compiles the code inside it only if DEBUG has been defined.

  5. #if defined(__APPLE__) — Conditional compilation based on the platform. Useful for code specific to a particular platform.

  6. #line 100 "custom_file.cpp" — Changes the line number and file name reported by the compiler on an error. Useful for debugging generated files.

  7. #error "message" — Stops compilation with an error message. Useful for ensuring certain conditions are met.

  8. #pragma message("...") — Displays a message during compilation. #pragma once prevents double inclusion (non-standard but widely supported).

  9. #define STRINGIFY(x) #x — The stringify operator. Converts a token into a string. STRINGIFY(Hello) → "Hello".

  10. #define CONCAT(a, b) a##b — The token-pasting operator. Joins two tokens. CONCAT(var, 10) → var10.

  11. __cplusplus, __FILE__, __LINE__ — Predefined macros provided by the compiler. Useful for debugging and logging.

  12. constexpr vs #define — constexpr has a data type, has scope, and is safe in templates. #define is 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.