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

[2026] 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)만 처리합니다. 아래 코드는 mermaid를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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>)로 헤더 존재 여부를 검사한다. 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

#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)

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

#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로 해제할 수 있다.

매크로 정의 해제

다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

#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이 있는 것과 같다.

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

여러 줄에 걸친 매크로는 줄 끝에 \를 붙여 이어 쓴다. \ 뒤에는 공백·주석이 없어야 한다. 다음은 간단한 cpp 코드 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

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

...로 가변 인자를 받고, __VA_ARGS__로 치환한다. 로깅·디버그 출력에 유용하다. 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

#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 제거

매크로 확장 순서

전처리기는 매크로를 한 번에 한 단계씩 확장한다. 중첩된 매크로는 바깥쪽부터 풀린다. 다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

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

#ifdef / #ifndef

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

#ifdef DEBUG
    std::cout << "Debug mode: value = " << value << "\n";
#endif
#ifndef NDEBUG
    // NDEBUG가 정의되지 않았으면 (디버그 모드)
    assert(ptr != nullptr);
#endif

#if / #elif / #else

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

플랫폼 분기

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

#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)는 동일하다. 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

#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 (전통적 방식)

아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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 (간단한 방식)

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 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 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 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))

인자와 전체를 괄호로 감싸는 이유: 연산자 우선순위로 인한 버그 방지. 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ 잘못된 예: #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) 관용구

아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

#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회 평가
타입타입 무관 (템플릿과 유사)타입 체크
디버깅심볼 없음, 단계별 실행 어려움일반 함수처럼 디버깅 가능
권장꼭 필요할 때만일반적으로 권장
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 위험: 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__은 전처리기가 현재 파일명현재 줄 번호로 치환하는 매크로다. 아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

출력 예시:

File: main.cpp, Line: 5

커스텀 Assert 매크로

아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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"이 출력된다.

디버그 로그 매크로

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

#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"

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

아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

#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 (#)

#을 매크로 인자 앞에 붙이면, 해당 인자가 문자열 리터럴로 변환된다. 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

#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 (##)

##은 두 토큰을 하나로 붙인다. 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

#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은 유효한 식별자다.

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

다음은 cpp를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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가 있는 헤더

아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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: 플랫폼별 코드

아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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: 디버그/릴리스 로깅

아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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: 버전 정보 주입

아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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”)

다음은 cpp를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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

아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

#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 통합

조건·에러 코드를 문자열로 변환하고, 심볼 이름을 동적으로 생성하는 예제다. 아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 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 없이 헤더가 여러 번 포함됨. 해결: 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 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는 모든 헤더에

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

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

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ✅ C++11 이후: constexpr 우선
constexpr double PI = 3.14159265359;
constexpr int MAX_BUFFER = 4096;
// 매크로는 조건부 컴파일, 플랫폼 분기 등에만
#ifdef DEBUG
#define LOG(msg) /* ....*/
#endif

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

아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ 가능하면 피함
#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)

아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

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

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

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

아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

BP8: Windows min/max 충돌 시 #undef

아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 코드를 직접 실행해보면서 동작을 확인해보세요.

#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에서 버전·빌드 정보 주입

아래 코드는 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}")

아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 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: 비활성화 가능한 디버그 출력

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

패턴 5: 컴파일러별 확장

아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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 경고

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

#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++ 전처리기, #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) 매크로 이름을 대문자로 구분해 충돌 방지.

관련 글

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3