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:
- File inclusion (
#include) - Macro expansion (
#define) - Conditional compilation (
#ifdef,#if) - 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:
#includeto 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:
- C++ Primer (Lippman et al.)
- GCC Preprocessor Documentation
- cppreference.com - Preprocessor
Related: macros, pragma, include.
In short: Preprocessor directives run before compilation and drive includes, macros, and conditional compilation.
Related posts (internal links)
- 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.
Keywords (search)
C++, preprocessor, macro, directive, include
Related posts
- C++ Preprocessor Tricks
- C++ Command Pattern
- C++ Header Guards
- C++ Macro Programming
- C++ Preprocessor Series