C++ Preprocessor Directives: #include, #define, #ifdef, and More

C++ Preprocessor Directives: #include, #define, #ifdef, and More

이 글의 핵심

Practical guide to preprocessor directives: includes, macros, conditional compilation, and collaboration with headers and include guards.

What are preprocessor directives?

Preprocessor directives are commands processed before compilation. They start with # and are used for file inclusion, macro definition, conditional compilation, and more.

#include <iostream>  // file inclusion
#define MAX 100      // macro definition
#ifdef DEBUG         // conditional compilation
    // debug code
#endif

Why they exist:

  • File inclusion: pull headers together
  • Conditional compilation: platform- or build-specific code
  • Macros: constants and textual substitution
  • Build configuration: debug vs release
// ❌ Without central config: duplicated blocks
// file1.cpp
void func() {
    #ifdef DEBUG
        std::cout << "debug\n";
    #endif
}

// file2.cpp
void func2() {
    #ifdef DEBUG
        std::cout << "debug\n";
    #endif
}

// ✅ Centralized with the preprocessor
// config.h
#ifdef DEBUG
    #define LOG(x) std::cout << x << '\n'
#else
    #define LOG(x)
#endif

// file1.cpp, file2.cpp
LOG("debug");

Preprocessor pipeline:

flowchart LR
    A[Source code] --> B[Preprocessor]
    B --> C[Preprocessed code]
    C --> D[Compiler]
    D --> E[Assembly]
    E --> F[Linker]
    F --> G[Executable]

Preprocessor order:

  1. File inclusion (#include)
  2. Macro expansion (#define)
  3. Conditional compilation (#ifdef, #if)
  4. Other directives (#pragma, #error)

Inspect preprocessor output:

# GCC/Clang
g++ -E file.cpp -o file.i

# MSVC
cl /E file.cpp

Main directives

// 1. #include
#include <iostream>   // system header
#include "myheader.h" // project header

// 2. #define
#define PI 3.14
#define MAX(a,b) ((a)>(b)?(a):(b))

// 3. #undef
#undef MAX

// 4. #ifdef, #ifndef
#ifdef DEBUG
    #define LOG(x) std::cout << x
#else
    #define LOG(x)
#endif

// 5. #if, #elif, #else
#if VERSION >= 2
    // version 2+
#elif VERSION == 1
    // version 1
#else
    // other
#endif

// 6. #pragma
#pragma once
#pragma pack(1)

Practical examples

Example 1: Include guards

// myheader.h
#ifndef MYHEADER_H
#define MYHEADER_H

class MyClass {
    // ...
};

#endif

// or #pragma once
#pragma once

class MyClass {
    // ...
};

Example 2: Conditional compilation

// config.h
#define DEBUG_MODE 1
#define PLATFORM_WINDOWS 1

// main.cpp
#include "config.h"

#if DEBUG_MODE
    #define LOG(x) std::cout << "[DEBUG] " << x << std::endl
#else
    #define LOG(x)
#endif

#ifdef PLATFORM_WINDOWS
    #include <windows.h>
#elif defined(PLATFORM_LINUX)
    #include <unistd.h>
#endif

int main() {
    LOG("program start");
}

Example 3: Macro functions

#define SQUARE(x) ((x) * (x))
#define MAX(a,b) ((a) > (b) ? (a) : (b))
#define MIN(a,b) ((a) < (b) ? (a) : (b))

int main() {
    int x = SQUARE(5);        // 25
    int max = MAX(10, 20);    // 20
    int min = MIN(10, 20);    // 10
}

Example 4: Stringizing and token pasting

#define STRINGIFY(x) #x
#define CONCAT(a,b) a##b

int main() {
    std::cout << STRINGIFY(Hello) << std::endl;  // "Hello"
    
    int xy = 10;
    int result = CONCAT(x, y);  // xy
}

Conditional compilation

// By platform
#ifdef _WIN32
    // Windows
#elif defined(__linux__)
    // Linux
#elif defined(__APPLE__)
    // macOS
#endif

// By compiler
#ifdef __GNUC__
    // GCC
#elif defined(_MSC_VER)
    // MSVC
#endif

// Debug vs release
#ifdef NDEBUG
    // release
#else
    // debug
#endif

Common pitfalls

Pitfall 1: Macro side effects

// ❌ Side effects
#define SQUARE(x) x * x
int result = SQUARE(1 + 2);  // 1 + 2 * 1 + 2 = 5

// ✅ Parentheses
#define SQUARE(x) ((x) * (x))
int result = SQUARE(1 + 2);  // 9

Pitfall 2: Missing include guards

// ❌ No guard
// myheader.h
class MyClass {};

// ✅ Add guards
#ifndef MYHEADER_H
#define MYHEADER_H
class MyClass {};
#endif

Pitfall 3: Macros vs functions

// ❌ Macro (not type-safe)
#define MAX(a,b) ((a)>(b)?(a):(b))

// ✅ Template function
template<typename T>
T max(T a, T b) {
    return a > b ? a : b;
}

Pitfall 4: #pragma once vs guards

// #pragma once (short)
#pragma once
class MyClass {};

// Include guards (portable)
#ifndef MYHEADER_H
#define MYHEADER_H
class MyClass {};
#endif

#pragma directives

// 1. #pragma once
#pragma once

// 2. #pragma pack
#pragma pack(push, 1)
struct Data {
    char c;
    int i;
};
#pragma pack(pop)

// 3. #pragma warning (MSVC)
#pragma warning(disable: 4996)

// 4. #pragma message
#pragma message("compile message")

// 5. #pragma omp (OpenMP)
#pragma omp parallel for
for (int i = 0; i < 100; i++) {
    // parallel
}

Real-world patterns

Pattern 1: Platform abstraction

// platform.h
#ifdef _WIN32
    #define EXPORT __declspec(dllexport)
    #define PATH_SEP '\\'
    #include <windows.h>
#elif defined(__linux__)
    #define EXPORT __attribute__((visibility("default")))
    #define PATH_SEP '/'
    #include <unistd.h>
#elif defined(__APPLE__)
    #define EXPORT __attribute__((visibility("default")))
    #define PATH_SEP '/'
    #include <TargetConditionals.h>
#endif

// Usage
EXPORT void myFunction() {
    std::string path = "dir" + std::string(1, PATH_SEP) + "file.txt";
}

Pattern 2: Debug logging

// debug.h
#ifdef DEBUG
    #define LOG(level, msg) \
        std::cout << "[" << level << "] " << __FILE__ << ":" << __LINE__ \
                  << " " << msg << '\n'
    #define ASSERT(cond) \
        if (!(cond)) { \
            std::cerr << "Assertion failed: " #cond << '\n'; \
            std::abort(); \
        }
#else
    #define LOG(level, msg)
    #define ASSERT(cond)
#endif

// Usage
void processData(int* data, size_t size) {
    ASSERT(data != nullptr);
    ASSERT(size > 0);
    
    LOG("INFO", "Processing " << size << " items");
    // ...
}

Pattern 3: Versioning

// version.h
#define VERSION_MAJOR 2
#define VERSION_MINOR 3
#define VERSION_PATCH 1

#if VERSION_MAJOR >= 2
    #define HAS_NEW_FEATURE 1
#endif

// api.cpp
void useAPI() {
    #ifdef HAS_NEW_FEATURE
        newFeature();
    #else
        oldFeature();
    #endif
}

FAQ

Q1: When do I use preprocessor directives?

A:

  • File inclusion: #include to compose headers
  • Conditional compilation: platform-specific code
  • Macros: constants and textual substitution

Q2: #pragma once vs include guards?

A:

  • #pragma once: short and fast (non-standard but widely supported)
  • Include guards: standard and portable

Q3: Macros vs functions?

A:

  • Macros: preprocess-time, not type-safe, hard to debug
  • Functions: type-safe and debuggable

Q4: #ifdef vs #if defined?

A:

  • #ifdef: simple conditions
  • #if defined: compound conditions

Q5: How do I see preprocessor output?

A: g++ -E file.cpp or cl /E file.cpp.

Q6: How do I avoid macro pitfalls?

A: Use enough parentheses.

Q7: Debugging macros?

A: Use #error and #warning.

Q8: Learning resources?

A:

Related: macros, pragma, include.

In short: Preprocessor directives run before compilation and drive includes, macros, and conditional compilation.


  • C++ Preprocessor Tricks
  • C++ Header Guards: #ifndef vs #pragma once
  • C++ Macro Programming

Practical tips

Debugging

  • Start with compiler warnings when something fails.
  • Reproduce with a small test case.

Performance

  • Do not optimize without profiling.
  • Set measurable goals first.

Code review

  • Check common review feedback early.
  • Follow team conventions.

Practical checklist

Before coding

  • Is this the right technique for the problem?
  • Can the team maintain it?
  • Does it meet performance needs?

While coding

  • All warnings fixed?
  • Edge cases handled?
  • Error handling OK?

During review

  • Intent clear?
  • Tests enough?
  • Docs adequate?

Use this checklist to improve quality.


C++, preprocessor, macro, directive, include


  • C++ Preprocessor Tricks
  • C++ Command Pattern
  • C++ Header Guards
  • C++ Macro Programming
  • C++ Preprocessor Series