C++ 전처리기 완벽 가이드 | #define·#ifdef

C++ 전처리기 완벽 가이드 | #define·#ifdef

이 글의 핵심

C++ 전처리기(#define, #ifdef, #include guard, 매크로 함수, __FILE__/__LINE__, stringification) 완벽 정리. 문제 시나리오, 일반적인 에러, 베스트 프랙티스, 프로덕션 패턴.

[C++ 실전 가이드 #5-1] C++ 전처리기 완벽 가이드

이 글을 읽으면 #define, #ifdef, include guard, 매크로 함수, __FILE__/__LINE__, stringification 등 전처리기 핵심 기능을 실전에서 활용할 수 있습니다.

#include 하나만 있어도 수천 줄이 삽입되고, #define으로 상수가 치환되며, #ifdef로 플랫폼별 코드가 분기됩니다. 비유하면 요리 전에 “재료 준비·손질”처럼, 컴파일러가 본격적으로 문법을 분석하기 전에 소스 코드를 정리·치환·조건부로 포함하는 단계입니다. 전처리기를 이해하면 헤더 중복 정의 에러를 막고, 디버그/릴리스 분기, 플랫폼별 코드를 효과적으로 관리할 수 있습니다.

이 글에서는 전처리기의 모든 핵심 기능을 문제 시나리오, 완전한 예제, 일반적인 에러, 베스트 프랙티스, 프로덕션 패턴까지 단계별로 다룹니다.

목차

  1. 전처리기 개요
  2. 문제 시나리오: 왜 전처리기를 알아야 하나?
  3. #define 기본과 상수 매크로
  4. 조건부 컴파일 (#ifdef, #if, #ifndef)
  5. Include Guard 완벽 가이드
  6. 매크로 함수 (함수형 매크로)
  7. __FILE__와 LINE (위치 정보)
  8. Stringification과 Token Pasting
  9. 완전한 전처리기 예제
  10. 자주 발생하는 에러와 해결법
  11. 베스트 프랙티스
  12. 프로덕션 패턴

1. 전처리기 개요

전처리기가 하는 일

전처리기는 컴파일 전에 소스 코드를 텍스트 단위로 변환하는 도구입니다. C++ 문법을 이해하지 않고, #으로 시작하는 지시문(directive)만 처리합니다.

flowchart LR
  A[.cpp 소스] --> B[전처리기]
  B --> C[.i 전처리 결과]
  C --> D[컴파일러]
  D --> E[.o 오브젝트]

주요 역할:

지시문역할
#include헤더 파일 내용 삽입
#define매크로 정의 (상수, 함수형)
#ifdef / #ifndef / #if조건부 컴파일
#pragma컴파일러별 확장 지시

전처리만 수행하여 결과를 확인하려면 -E 옵션을 사용합니다:

g++ -E main.cpp -o main.i

2. 문제 시나리오: 왜 전처리기를 알아야 하나?

시나리오 1: “redefinition of ‘class X’” 에러

상황: config.hmain.cpputils.cpp에서 모두 include했더니 “클래스 X가 중복 정의되었다”는 에러가 난다.

원인: 헤더에 클래스·함수 정의가 있고, 여러 .cpp에서 include할 때마다 같은 정의가 복사된다. 링킹 시 같은 심볼이 두 번 정의된 것으로 처리된다.

해결: Include guard (#ifndef/#define/#endif 또는 #pragma once)로 헤더가 한 번만 포함되도록 한다.

시나리오 2: Windows와 Linux에서 다른 API 사용

상황: 같은 소스로 Windows와 Linux를 빌드해야 하는데, CreateFile(Windows)와 open(Linux) 등 API가 다르다.

원인: 플랫폼별로 다른 코드가 필요하다.

해결: #ifdef _WIN32 / #else / #endif로 조건부 컴파일. 빌드 시 -DPLATFORM_WIN 같은 매크로로 분기한다.

시나리오 3: 디버그 빌드에서만 로그 출력

상황: 디버그 시에는 printf로 상세 로그를 찍고, 릴리스에서는 로그를 완전히 제거해 성능을 유지하고 싶다.

원인: if (debug) printf(...)는 런타임 분기라, 릴리스에서도 분기·함수 호출 비용이 남는다.

해결: #ifdef DEBUG로 디버그 시에만 로그 코드를 포함. 릴리스 빌드에서는 해당 코드가 아예 컴파일되지 않아 오버헤드가 없다.

시나리오 4: assert 실패 시 파일명·줄 번호 표시

상황: assert(x > 0) 실패 시 “어느 파일 몇 번째 줄에서 실패했는지” 알려주고 싶다.

원인: assert 구현에 __FILE__, __LINE__ 같은 전처리기 매크로가 필요하다.

해결: #define MY_ASSERT(cond) do { if (!(cond)) report(__FILE__, __LINE__); } while(0) 형태로 매크로를 정의한다.

시나리오 5: 빌드 버전·날짜 자동 주입

상황: 실행 파일에 “버전 1.2.3, 빌드 2026-03-11” 같은 정보를 넣고 싶다.

원인: 소스 코드에 하드코딩하면 매번 수정해야 한다.

해결: g++ -DVERSION=\"1.2.3\" -DBUILD_DATE=\"2026-03-11\" main.cpp처럼 컴파일 시 -D로 매크로 주입. CI/CD에서 git describe·date 결과를 전달한다.

시나리오 6: 매크로로 인한 예상치 못한 동작

상황: #define max(a,b) ((a)>(b)?(a):(b))를 사용했는데, max(i++, j++) 호출 시 ij가 두 번 증가한다.

원인: 매크로는 텍스트 치환이라, 인자가 여러 번 평가될 수 있다.

해결: 인라인 함수·std::max 사용. 매크로를 쓸 수밖에 없다면 인자를 괄호로 감싸고, 전체를 괄호로 감싸는 등 주의해서 작성한다.

시나리오 7: 헤더 순환 의존

상황: A.hB.h를 include하고, B.hA.h를 include해 “incomplete type” 에러가 난다.

원인: Include guard가 없거나, 순환 의존 구조 자체가 문제다.

해결: Include guard로 중복 포함 방지. 전방 선언으로 의존성을 끊고, 필요한 헤더는 .cpp에서만 include한다.

시나리오 8: 외부 라이브러리 매크로와 이름 충돌

상황: Windows SDK의 min/max 매크로 때문에 std::min/std::max 사용 시 컴파일 에러가 난다.

원인: <windows.h>#define min(a,b) ...를 포함. std::minstd::(a,b) 형태로 치환되어 문법 오류.

해결: #define NOMINMAX#include <windows.h> 앞에 정의. 또는 (std::min)처럼 괄호로 감싸 매크로 확장을 막는다.

시나리오 9: C++ 표준 버전별 기능 분기

상황: C++17에서는 std::optional을 쓰고, C++14 이하에서는 자체 구현체를 쓰고 싶다.

원인: __cplusplus 매크로 값이 컴파일러·표준에 따라 다르다. C++17은 201703L, C++20은 202002L이다.

해결: #if __cplusplus >= 201703L로 조건부 include. 또는 #if __has_include(<optional>)로 헤더 존재 여부를 검사한다.

#if __cplusplus >= 201703L
    #include <optional>
    using std::optional;
#else
    #include "optional_polyfill.hpp"
#endif

시나리오 10: 매크로 인자에 쉼표가 포함된 경우

상황: LOG(std::map<int, int> m)처럼 매크로 인자에 쉼표가 있으면, 전처리기가 std::map<intint> m을 별도 인자로 잘못 분리한다.

원인: 전처리기는 괄호 깊이를 추적하지만, 템플릿·함수 호출 내부의 쉼표를 인자 구분자로 인식한다.

해결: typedef std::map<int, int> IntMap으로 타입 별칭을 만들거나, LOG((std::map<int, int>))처럼 인자를 이중 괄호로 감싼다. 가변 인자 매크로 LOG(...)를 쓰는 것도 방법이다.


3. #define 기본과 상수 매크로

단순 치환 (Object-like Macro)

#define PI 3.14159265359
#define APP_NAME "MyApp"
#define MAX_BUFFER 4096

int main() {
    double area = PI * 10 * 10;  // 3.14159265359 * 10 * 10로 치환
    return 0;
}

주의: 전처리기는 문법을 모른다. PI가 나오는 모든 곳을 그대로 3.14159265359로 바꾼다. #define PI 3.14 다음에 #undef PI로 해제할 수 있다.

매크로 정의 해제

#define TEMP_DEBUG 1
// ... TEMP_DEBUG 사용 ...
#undef TEMP_DEBUG
// 이후 TEMP_DEBUG는 정의되지 않음

컴파일 시 매크로 정의 (-D 옵션)

g++ -DDEBUG -DMAX_SIZE=100 main.cpp -o main

이렇게 하면 소스에 #define DEBUG 1, #define MAX_SIZE 100이 있는 것과 같다.

다중 줄 매크로 (백슬래시 연속)

여러 줄에 걸친 매크로는 줄 끝에 \를 붙여 이어 쓴다. \ 뒤에는 공백·주석이 없어야 한다.

#define LOG_START() \
    do { \
        std::cout << "=== Start " << __FUNCTION__ << " ===" << "\n"; \
    } while(0)

가변 인자 매크로 (Variadic Macro, C++11)

...로 가변 인자를 받고, __VA_ARGS__로 치환한다. 로깅·디버그 출력에 유용하다.

#define LOG(fmt, ...) \
    printf("[%s:%d] " fmt "\n", __FILE__, __LINE__, __VA_ARGS__)

// 사용
LOG("value=%d, name=%s", 42, "test");  // [main.cpp:10] value=42, name=test

주의: __VA_ARGS__가 비어 있으면 printf(..., )처럼 trailing comma가 생겨 C++에서 에러가 날 수 있다. ##__VA_ARGS__로 빈 경우 comma를 제거할 수 있다 (GCC 확장).

#define LOG2(fmt, ...) printf(fmt "\n", ##__VA_ARGS__)
LOG2("no args");   // printf("no args\n", ) → ##__VA_ARGS__가 빈 경우 comma 제거

매크로 확장 순서

전처리기는 매크로를 한 번에 한 단계씩 확장한다. 중첩된 매크로는 바깥쪽부터 풀린다.

#define A 1
#define B A
#define C B
// C → B → A → 1

4. 조건부 컴파일 (#ifdef, #if, #ifndef)

#ifdef / #ifndef

#ifdef DEBUG
    std::cout << "Debug mode: value = " << value << "\n";
#endif

#ifndef NDEBUG
    // NDEBUG가 정의되지 않았으면 (디버그 모드)
    assert(ptr != nullptr);
#endif

#if / #elif / #else

#if __cplusplus >= 202002L
    // C++20 이상
    #define USE_COROUTINES 1
#elif __cplusplus >= 201703L
    // C++17
    #define USE_COROUTINES 0
#else
    #define USE_COROUTINES 0
#endif

플랫폼 분기

#ifdef _WIN32
    #include <windows.h>
    #define PLATFORM_API __declspec(dllexport)
#elif defined(__linux__)
    #include <unistd.h>
    #define PLATFORM_API __attribute__((visibility("default")))
#else
    #define PLATFORM_API
#endif

자주 쓰는 플랫폼 매크로:

매크로의미
_WIN32Windows (32/64 공통)
_WIN64Windows 64비트
__linux__Linux
__APPLE__macOS, iOS
__ANDROID__Android

defined() 연산자

#if 안에서 defined(매크로)로 정의 여부를 확인한다. #ifdef X#if defined(X)는 동일하다.

#if defined(DEBUG) && defined(VERBOSE)
    #define LOG_VERBOSE(msg) std::cerr << (msg) << "\n"
#elif defined(DEBUG)
    #define LOG_VERBOSE(msg) ((void)0)
#else
    #define LOG_VERBOSE(msg) ((void)0)
#endif

여러 매크로를 한 번에 검사할 때 유용하다.

#if defined(_WIN32) || defined(_WIN64)
    #define WINDOWS 1
#endif

5. Include Guard 완벽 가이드

문제: 헤더 중복 포함

A.cppB.hC.h를 include하고, B.hC.h가 둘 다 common.h를 include하면, common.h 내용이 두 번 삽입된다. 클래스·함수 정의가 중복되면 redefinition 에러가 난다.

해결 1: #ifndef / #define / #endif (전통적 방식)

// config.h
#ifndef CONFIG_H
#define CONFIG_H

#define APP_VERSION "1.0.0"
#define MAX_BUFFER_SIZE 4096

struct Config {
    int timeout;
};

#endif  // CONFIG_H

동작: 첫 include 시 CONFIG_H가 정의되지 않았으므로 #ifndef 블록이 실행되고, CONFIG_H를 정의한다. 두 번째 include 시에는 CONFIG_H가 이미 정의되어 있으므로 블록 전체가 건너뛴다.

해결 2: #pragma once (간단한 방식)

// config.h
#pragma once

#define APP_VERSION "1.0.0"
#define MAX_BUFFER_SIZE 4096

장점: 한 줄로 끝. 단점: 표준이 아니라 컴파일러 확장이지만, GCC, Clang, MSVC 모두 지원한다.

Include Guard 네이밍 규칙

  • 헤더 파일명을 대문자로, ._로 바꾸고 _H 또는 _HPP 접미사 붙이기: config.hCONFIG_H
  • 프로젝트 prefix를 붙여 충돌 방지: PKGLOG_CONFIG_H
// pkglog_config.h
#ifndef PKGLOG_CONFIG_H
#define PKGLOG_CONFIG_H
// ...
#endif

권장: #pragma once가 간단하고 대부분 환경에서 지원. 레거시·이식성 극대화가 필요하면 #ifndef 방식 사용.


6. 매크로 함수 (함수형 매크로)

기본 문법

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

인자와 전체를 괄호로 감싸는 이유: 연산자 우선순위로 인한 버그 방지.

// ❌ 잘못된 예: #define SQUARE(x) x * x
int y = SQUARE(2 + 3);  // 2 + 3 * 2 + 3 = 11 (의도: 25)

// ✅ 올바른 예: #define SQUARE(x) ((x) * (x))
int y = SQUARE(2 + 3);  // ((2 + 3) * (2 + 3)) = 25

다중 문장 매크로: do-while(0) 관용구

#define LOG_AND_RETURN(msg) \
    do { \
        std::cerr << (msg) << "\n"; \
        return -1; \
    } while(0)

// 사용
if (error) LOG_AND_RETURN("Failed");

do-while(0)를 쓰는 이유: if (cond) LOG_AND_RETURN(...); else do_something();처럼 쓸 때, 매크로가 단일 문장처럼 동작하게 한다. do { ... } while(0) 뒤에 ;를 붙여도 문법상 하나의 문장이다.

매크로 vs 인라인 함수

구분매크로인라인 함수
평가인자가 여러 번 평가될 수 있음인자 1회 평가
타입타입 무관 (템플릿과 유사)타입 체크
디버깅심볼 없음, 단계별 실행 어려움일반 함수처럼 디버깅 가능
권장꼭 필요할 때만일반적으로 권장
// ❌ 위험: max(i++, j++)에서 i 또는 j가 두 번 증가
#define MAX(a, b) ((a) > (b) ? (a) : (b))

// ✅ 안전: std::max 또는 인라인 함수 사용
template<typename T>
inline T my_max(T a, T b) { return a > b ? a : b; }

7. __FILE__와 LINE (위치 정보)

기본 사용

__FILE____LINE__은 전처리기가 현재 파일명현재 줄 번호로 치환하는 매크로다.

#include <iostream>

int main() {
    std::cout << "File: " << __FILE__ << ", Line: " << __LINE__ << "\n";
    return 0;
}

출력 예시:

File: main.cpp, Line: 5

커스텀 Assert 매크로

#define MY_ASSERT(cond) \
    do { \
        if (!(cond)) { \
            std::cerr << "Assertion failed: " #cond \
                      << " at " << __FILE__ << ":" << __LINE__ << "\n"; \
            std::abort(); \
        } \
    } while(0)

// 사용
MY_ASSERT(ptr != nullptr);

#condstringification으로, cond를 문자열 리터럴로 바꾼다. MY_ASSERT(x > 0)이면 "x > 0"이 출력된다.

디버그 로그 매크로

#define DEBUG_LOG(msg) \
    std::cerr << "[" << __FILE__ << ":" << __LINE__ << "] " << (msg) << "\n"

#ifdef DEBUG
    #define LOG(msg) DEBUG_LOG(msg)
#else
    #define LOG(msg) ((void)0)  // 릴리스에서는 아무것도 하지 않음
#endif

__func__와 조합

__func__는 C++11 표준 매크로로, 현재 함수 이름을 문자열로 제공한다.

#define LOG_FUNC(msg) \
    std::cerr << "[" << __FILE__ << ":" << __LINE__ << " in " << __func__ << "] " \
              << (msg) << "\n"

예외 메시지에 위치 정보 포함

#define THROW_IF(cond, msg) \
    do { \
        if (cond) throw std::runtime_error( \
            std::string(__FILE__) + ":" + std::to_string(__LINE__) + ": " + (msg)); \
    } while(0)

8. Stringification과 Token Pasting

Stringification (#)

#을 매크로 인자 앞에 붙이면, 해당 인자가 문자열 리터럴로 변환된다.

#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)

int main() {
    std::cout << STRINGIFY(hello) << "\n";   // "hello"
    std::cout << TOSTRING(__LINE__) << "\n"; // "7" (__LINE__ 값이 먼저 치환됨)
    return 0;
}

2단계 매크로가 필요한 이유: STRINGIFY(__LINE__)"__LINE__"이 되지만, TOSTRING(__LINE__)STRINGIFY(7)"7"이 된다. __LINE__을 먼저 확장한 뒤 stringify하려면 한 단계 더 거쳐야 한다.

Token Pasting (##)

##은 두 토큰을 하나로 붙인다.

#define CONCAT(a, b) a##b
#define MAKE_VAR(name, num) name##num

int main() {
    int xy = 10;
    int MAKE_VAR(value, 1) = 42;  // int value1 = 42;
    std::cout << CONCAT(x, y) << "\n";  // xy → 10
    return 0;
}

Token Pasting 주의사항

## 결과는 유효한 전처리 토큰이어야 한다. CONCAT(var, 123)var123은 유효한 식별자다.

실전 예: 에러 메시지 매크로

#define CHECK(cond, msg) \
    do { \
        if (!(cond)) { \
            std::cerr << "Error at " << __FILE__ << ":" << __LINE__ \
                      << ": " << (msg) << "\n"; \
            std::abort(); \
        } \
    } while(0)

#define CHECK_EQ(a, b) \
    do { \
        if ((a) != (b)) { \
            std::cerr << "Assertion " #a " == " #b " failed at " \
                      << __FILE__ << ":" << __LINE__ \
                      << ": " << (a) << " != " << (b) << "\n"; \
            std::abort(); \
        } \
    } while(0)

9. 완전한 전처리기 예제

예제 1: Include Guard가 있는 헤더

// math_utils.h
#ifndef MATH_UTILS_H
#define MATH_UTILS_H

#define PI 3.14159265359
#define DEG_TO_RAD(deg) ((deg) * PI / 180.0)

inline double circle_area(double r) {
    return PI * r * r;
}

#endif  // MATH_UTILS_H

예제 2: 플랫폼별 코드

// platform_io.h
#ifndef PLATFORM_IO_H
#define PLATFORM_IO_H

#ifdef _WIN32
    #include <windows.h>
    #define SLEEP_MS(ms) Sleep(ms)
#else
    #include <unistd.h>
    #define SLEEP_MS(ms) usleep((ms) * 1000)
#endif

#endif

예제 3: 디버그/릴리스 로깅

// logger.h
#ifndef LOGGER_H
#define LOGGER_H

#include <iostream>

#ifdef DEBUG
    #define LOG(msg) std::cerr << "[" << __FILE__ << ":" << __LINE__ << "] " << (msg) << "\n"
#else
    #define LOG(msg) ((void)0)
#endif

#endif

예제 4: 버전 정보 주입

// main.cpp
#include <iostream>

#ifndef VERSION
#define VERSION "unknown"
#endif
#ifndef BUILD_DATE
#define BUILD_DATE __DATE__
#endif

int main() {
    std::cout << "App v" << VERSION << " built " << BUILD_DATE << "\n";
    return 0;
}
g++ -DVERSION=\"1.2.3\" -DBUILD_DATE=\"2026-03-11\" main.cpp -o main

예제 5: C/C++ 혼용 (extern “C”)

// mylib.h
#ifndef MYLIB_H
#define MYLIB_H

#ifdef __cplusplus
extern "C" {
#endif

void c_function(int x);

#ifdef __cplusplus
}
#endif

#endif

예제 6: FILE/LINE 활용 Assert

#define ASSERT(cond) \
    do { \
        if (!(cond)) { \
            std::cerr << "Assertion `" #cond "` failed at " << __FILE__ << ":" << __LINE__ << "\n"; \
            std::abort(); \
        } \
    } while(0)
// 사용: ASSERT(x > 0);  // 실패 시 파일명·줄 번호 출력

예제 7: 플랫폼별 스레드 로컬 스토리지

Windows: __declspec(thread), TlsAlloc/TlsGetValue. POSIX: __thread, pthread_key_create/pthread_getspecific. #ifdef _WIN32로 분기.

예제 8: 빌드 정보 자동 생성

version.hAPP_VERSION_MAJOR, BUILD_DATE(__DATE__), BUILD_TIME(__TIME__) 정의. CI에서 자동 생성.

예제 9: Stringification과 Token Pasting 통합

조건·에러 코드를 문자열로 변환하고, 심볼 이름을 동적으로 생성하는 예제다.

// error_codes.h
#ifndef ERROR_CODES_H
#define ERROR_CODES_H

#define ERROR_CODE(e) e
#define ERROR_NAME(e) #e
#define ERROR_MSG(e) "Error: " #e " (code=" ERROR_NAME(e) ")"

#define ERR_SUCCESS 0
#define ERR_INVALID_ARG 1
#define ERR_OUT_OF_MEMORY 2

// 사용: ERROR_MSG(ERR_INVALID_ARG) → "Error: ERR_INVALID_ARG (code=ERR_INVALID_ARG)"
#endif
// enum_from_macro.cpp - Token pasting으로 enum 값 생성
#define DECLARE_FLAG(name) name##_flag

enum Flags {
    DECLARE_FLAG(read) = 1,
    DECLARE_FLAG(write) = 2,
    DECLARE_FLAG(execute) = 4
};
// read_flag, write_flag, execute_flag 생성

예제 10: __has_include로 선택적 헤더 포함 (C++17)

#if __has_include(<optional>)로 헤더 존재 여부를 검사한 뒤 조건부 include. C++17 미만에서는 <experimental/optional>을 시도한다.

예제 11: 가변 인자 로그 매크로

LOG(fmt, ...)printf 스타일 로그. ##__VA_ARGS__로 인자 없을 때 trailing comma 제거.

예제 12: 전처리 결과 확인

g++ -E -P main.cpp -o main.i로 전처리만 수행. g++ -E -dM -x c++ /dev/null로 정의된 매크로 목록 확인.


10. 자주 발생하는 에러와 해결법

에러 1: redefinition of ‘class/struct X’

에러 메시지 예시:

In file included from main.cpp:2:
config.h:5: error: redefinition of 'struct Config'

원인: Include guard 없이 헤더가 여러 번 포함됨.

해결:

// config.h
#ifndef CONFIG_H
#define CONFIG_H
// ... 내용 ...
#endif

에러 2: macro expansion 시 예상치 못한 결과

상황: #define SQUARE(x) x*x 사용 시 SQUARE(2+3)이 11이 됨.

원인: 괄호 없이 치환되어 2+3*2+3이 됨.

해결:

#define SQUARE(x) ((x) * (x))

에러 3: 매크로 인자 다중 평가

상황: #define MAX(a,b) ((a)>(b)?(a):(b))에서 MAX(i++, j++) 호출 시 i, j가 두 번씩 증가.

원인: 매크로는 텍스트 치환이라 (a), (b)가 각각 두 번 나오면 두 번 평가됨.

해결: std::max 또는 인라인 함수 사용. 매크로를 쓸 수밖에 없다면 부수 효과가 있는 인자 사용 금지.

에러 4: ’#’ is not followed by a macro parameter

상황: #define LOG(x) # x처럼 #과 인자 사이에 공백이 있음.

원인: #은 반드시 매크로 인자 바로 앞에 와야 함.

해결:

#define LOG(x) #x  // 공백 제거

에러 5: paste 연산자(##)로 유효하지 않은 토큰 생성

상황: #define CONCAT(a,b) a##b에서 CONCAT(1,2)12가 되어 정수 리터럴이 깨짐.

원인: ## 결과가 유효한 토큰이어야 함. 12는 유효한 정수 리터럴이지만, int CONCAT(my, var)int myvar로 올바르게 동작한다. 문제는 CONCAT(+, -)+- 같은 비유효 토큰.

해결: ## 사용 시 결과가 유효한 식별자·숫자 등이 되도록 설계.

에러 6: -D 옵션으로 전달한 값에 따옴표 누락

상황: g++ -DVERSION=1.2.3 main.cppstd::cout << VERSION1.2.3으로 나오지만, "1.2.3" 문자열로 쓰고 싶을 때.

원인: -DVERSION=1.2.3#define VERSION 1.2.3과 같아서 VERSION은 숫자/식별자로 치환됨. 문자열로 쓰려면 -DVERSION=\"1.2.3\"처럼 이스케이프된 따옴표 필요.

해결:

g++ -DVERSION=\"1.2.3\" main.cpp

에러 7: #include “file” vs #include 혼동

상황: #include "myheader"가 시스템 경로에서 찾지 못함.

원인: "..."는 현재 디렉터리·-I 경로부터 검색, <...>는 시스템 경로만 검색.

해결: 프로젝트 헤더는 "...", 표준 라이브러리는 <...> 사용. -I로 프로젝트 include 경로 추가.

에러 8: 매크로와 변수 이름 충돌

상황: #define max 100std::max(a, b) 사용 시 에러.

원인: max가 100으로 치환되어 std::100(a, b)가 됨.

해결: 매크로 이름을 대문자·prefix로 구분 (MAX_BUFFER, APP_MAX). #undef로 필요한 구간에서만 사용.

에러 9: do-while(0) 누락

#define FOO() { a(); b(); } 사용 시 if (x) FOO(); else bar();에서 else 매칭 오류. do { } while(0) 사용.

에러 10: 매크로 인자에 쉼표

F(std::map<int,int>, x)에서 전처리기가 std::map<intint>, x로 잘못 분리. typedef 또는 이중 괄호로 해결.

에러 11: LINE 문자열화

STRINGIFY(__LINE__)"__LINE__"이 됨. TOSTRING(__LINE__)처럼 2단계 매크로로 __LINE__을 먼저 확장한 뒤 stringify.

에러 12~15: 기타 주의사항

  • 백슬래시 뒤 공백: 다중 줄 매크로에서 \ 직후 줄바꿈, 공백·주석 금지.
  • Include guard 충돌: config.hconfig_impl.h가 같은 guard를 쓰면 안 됨. CONFIG_H, CONFIG_IMPL_H처럼 구분.
  • #if 0 내부 중첩: #ifdef가 있으면 #endif 매칭이 꼬일 수 있음. #ifdef NEVER_DEFINED 또는 블록 주석 사용.
  • 매크로 재정의: #ifndef PI / #define PI 3.14 / #endif로 한 번만 정의.

에러 요약 표

에러 유형원인해결
redefinitionInclude guard 없음#ifndef/#define/#endif 추가
잘못된 매크로 확장괄호 누락인자·전체에 괄호 추가
인자 다중 평가함수형 매크로인라인 함수·std::max 사용
# 문법 오류#과 인자 사이 공백#x 형태로 붙여 쓰기
-D 문자열 전달따옴표 누락-DVAR=\"value\"
이름 충돌매크로와 식별자 겹침대문자·prefix 사용
else 매칭 오류다중 문장 매크로에 { } 사용do { } while(0) 사용
쉼표 인자 분리매크로 인자에 템플릿 등typedef·이중 괄호
LINE 문자열화1단계 stringify2단계 TOSTRING 매크로

11. 베스트 프랙티스

BP1: Include Guard는 모든 헤더에

// ✅ 모든 .h 파일
#ifndef UNIQUE_HEADER_NAME_H
#define UNIQUE_HEADER_NAME_H
// ...
#endif

BP2: 상수는 constexpr 우선, 매크로는 보조

// ✅ C++11 이후: constexpr 우선
constexpr double PI = 3.14159265359;
constexpr int MAX_BUFFER = 4096;

// 매크로는 조건부 컴파일, 플랫폼 분기 등에만
#ifdef DEBUG
#define LOG(msg) /* ... */
#endif

BP3: 함수형 매크로 대신 인라인/템플릿

// ❌ 가능하면 피함
#define MAX(a, b) ((a) > (b) ? (a) : (b))

// ✅ 권장
template<typename T>
inline T max_val(T a, T b) { return a > b ? a : b; }
// 또는
#include <algorithm>
std::max(a, b);

BP4: 매크로 이름은 대문자·prefix

#define PKGLOG_MAX_BUFFER 4096
#define PKGLOG_DEBUG_LOG(msg) /* ... */

BP5: 다중 문장 매크로는 do-while(0)

#define SAFE_DELETE(p) \
    do { \
        delete (p); \
        (p) = nullptr; \
    } while(0)

BP6: 디버그 전용 코드는 #ifdef로 제거

#ifdef DEBUG
    #define ASSERT(cond) /* ... */
#else
    #define ASSERT(cond) ((void)0)
#endif

BP7: 플랫폼 코드는 별도 헤더로 분리

// platform.h
#ifdef _WIN32
#include "platform_win.h"
#else
#include "platform_posix.h"
#endif

BP8: Windows min/max 충돌 시 #undef

#include <windows.h>
#ifdef min
#undef min
#endif
#ifdef max
#undef max
#endif

BP9: 매크로 사용 범위 최소화

필요한 블록 안에서만 정의하고 #undef로 해제. #endif 뒤에 // _WIN32처럼 조건 주석을 붙이면 유지보수가 쉬워진다.

BP10: 매크로 vs 대안 선택

조건부 컴파일·__FILE__/__LINE__·Include guard는 매크로 필수. 상수·단순 함수는 constexpr·inline 우선.


12. 프로덕션 패턴

패턴 1: CMake에서 버전·빌드 정보 주입

# CMakeLists.txt
execute_process(
    COMMAND git describe --tags --always
    OUTPUT_VARIABLE GIT_VERSION
    OUTPUT_STRIP_TRAILING_WHITESPACE
)
string(TIMESTAMP BUILD_DATE "%Y-%m-%d")
add_definitions(-DVERSION="${GIT_VERSION}" -DBUILD_DATE="${BUILD_DATE}")
// main.cpp
#include <iostream>
int main() {
    std::cout << "Version: " << VERSION << ", Built: " << BUILD_DATE << "\n";
    return 0;
}

패턴 2: 단계별 로그 레벨

LOG_LEVEL_ERROR(1)~LOG_LEVEL_DEBUG(4) 정의. CURRENT_LOG_LEVELLOG_ERROR/LOG_DEBUG 분기. -DCURRENT_LOG_LEVEL=4로 디버그, =1로 릴리스.

패턴 3: 단위 테스트용 매크로

TEST_EQ(actual, expected)__FILE__/__LINE__ 포함 실패 메시지 출력. do-while(0)로 단일 문장처럼 사용.

패턴 4: 비활성화 가능한 디버그 출력

#ifdef ENABLE_TRACE
    #define TRACE(msg) std::cout << "[TRACE] " << (msg) << "\n"
#else
    #define TRACE(msg) ((void)0)
#endif

패턴 5: 컴파일러별 확장

#ifdef __GNUC__
    #define LIKELY(x)   __builtin_expect(!!(x), 1)
    #define UNLIKELY(x) __builtin_expect(!!(x), 0)
#else
    #define LIKELY(x)   (x)
    #define UNLIKELY(x) (x)
#endif

if (UNLIKELY(ptr == nullptr)) {
    handle_error();
}

패턴 6: deprecated 경고

#ifdef __GNUC__
    #define DEPRECATED __attribute__((deprecated))
#elif defined(_MSC_VER)
    #define DEPRECATED __declspec(deprecated)
#else
    #define DEPRECATED
#endif

DEPRECATED void old_api();

패턴 7: Export 매크로 (DLL/공유 라이브러리)

Windows: MYLIB_EXPORTSdllexport, 아니면 dllimport. POSIX: __attribute__((visibility("default"))).

패턴 8: 전처리 결과 검증

g++ -E -dM main.cpp | grep -E "^#define"로 정의된 매크로 확인.

패턴 9: 기능 플래그 (Feature Flag)

#define FEATURE_NEW_UI 1로 배포 전 기능을 켜고 끔. #if FEATURE_NEW_UI로 분기.

패턴 10: 컴파일 타임 버전 검사

#if __cplusplus < 201703L / #error "C++17 required" / #endif로 최소 표준 강제.

패턴 11: X-Macro로 enum과 문자열 동기화

enum 값과 대응 문자열을 .def 파일에서 한 번만 정의해 중복을 줄인다. #define X(name, msg)#include "xmacro.def"로 enum과 switch를 생성.

패턴 12: 빌드 프로파일별 매크로

CMake에서 CMAKE_BUILD_TYPE이 Debug면 -DDEBUG, Release면 -DNDEBUGadd_definitions로 주입한다.

패턴 13: 단위 테스트 전용 매크로

#ifdef RUNNING_TESTS로 테스트 빌드에서만 TEST_ASSERTthrow TestFailure로 동작하도록 분리.

패턴 14: 헤더 포함 순서 표준화

전처리기 관련 에러를 줄이기 위해 프로젝트에서 헤더 포함 순서를 정한다: 1) 대응 .h, 2) C 표준, 3) C++ 표준, 4) 서드파티, 5) 프로젝트 내부.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ 컴파일 과정 | “undefined reference” 에러가 나는 이유 (전처리·링킹 4단계)
  • C++ constexpr 함수와 변수 | 컴파일 타임에 계산하기 [#26-1]
  • C++ 로깅·Assertion | 프로덕션 간헐적 크래시, 로그 없이 재현 불가일 때

이 글에서 다루는 키워드 (관련 검색어)

C++ 전처리기, #define 매크로, #ifdef 조건부 컴파일, include guard, #pragma once, FILE LINE, stringification, 매크로 함수, 플랫폼별 코드 등으로 검색하시면 이 글이 도움이 됩니다.


마무리

핵심 요약

#define: 상수·함수형 매크로. 인자와 전체를 괄호로 감싸고, 부수 효과 있는 인자 사용 금지.
#ifdef / #ifndef / #if: 조건부 컴파일. 플랫폼 분기, 디버그/릴리스 분리.
Include guard: #ifndef/#define/#endif 또는 #pragma once로 헤더 중복 포함 방지.
FILE, LINE: assert·로그에서 파일명·줄 번호 출력.
Stringification (#): 매크로 인자를 문자열로 변환. 2단계 매크로로 __LINE__ 확장 후 변환.
Token pasting (##): 두 토큰을 하나로 붙임.

구현 체크리스트

  • 모든 헤더에 include guard 적용
  • 상수는 constexpr 우선, 매크로는 조건부 컴파일 등에만
  • 함수형 매크로 대신 인라인 함수·std::max 사용
  • 다중 문장 매크로는 do-while(0) 사용
  • 매크로 이름 대문자·prefix로 충돌 방지
  • -D 옵션으로 빌드 시 매크로 주입
  • 전처리 결과는 g++ -E로 검증

다음 글

전처리기를 이해했다면, 컴파일 과정의 다음 단계인 컴파일·어셈블·링킹을 복습하거나, constexpr로 컴파일 타임 상수를 더 안전하게 다루는 방법을 배워보세요.


자주 묻는 질문 (FAQ)

Q. #pragma once와 include guard 중 뭘 써야 하나요?

A. #pragma once가 간단하고 대부분의 컴파일러에서 지원됩니다. 크로스 플랫폼·오래된 컴파일러 지원이 필요하면 #ifndef/#define/#endif를 사용하세요.

Q. 매크로와 constexpr/템플릿의 차이는?

A. 매크로는 전처리 단계에서 텍스트 치환되고, constexpr·템플릿은 컴파일 단계에서 처리됩니다. 타입 체크·디버깅·네임스페이스 등에서 constexpr·템플릿이 유리합니다. 매크로는 조건부 컴파일·__FILE__/__LINE__ 등 전처리기만 가능한 경우에 사용하세요.

Q. __FILE__이 전체 경로로 나오는데 상대 경로로 바꿀 수 있나요?

A. 컴파일러마다 다릅니다. GCC/Clang은 -fmacro-prefix-map=old=new로 경로를 매핑할 수 있습니다. CMake에서는 add_compile_options(-fmacro-prefix-map=${CMAKE_SOURCE_DIR}=.)로 소스 루트를 제거할 수 있습니다.

Q. 매크로로 인한 버그를 어떻게 예방하나요?

A. 1) 가능하면 인라인 함수·템플릿 사용, 2) 매크로는 괄호로 감싸기, 3) 부수 효과 있는 인자 금지, 4) 매크로 이름을 대문자로 구분해 충돌 방지.

관련 글

  • C++ 컴파일 과정 |
  • C++ 전처리기 |
  • C++ GDB 기초 완벽 가이드 | 브레이크포인트·워치포인트
  • C++ LLDB 기초 완벽 가이드 | macOS·브레이크포인트
  • CMake 입문 | 수십 개 파일 컴파일할 때 필요한 빌드 자동화 (CMakeLists.txt 기초)