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로 플랫폼별 코드가 분기됩니다. 비유하면 요리 전에 “재료 준비·손질”처럼, 컴파일러가 본격적으로 문법을 분석하기 전에 소스 코드를 정리·치환·조건부로 포함하는 단계입니다. 전처리기를 이해하면 헤더 중복 정의 에러를 막고, 디버그/릴리스 분기, 플랫폼별 코드를 효과적으로 관리할 수 있습니다.
이 글에서는 전처리기의 모든 핵심 기능을 문제 시나리오, 완전한 예제, 일반적인 에러, 베스트 프랙티스, 프로덕션 패턴까지 단계별로 다룹니다.
목차
- 전처리기 개요
- 문제 시나리오: 왜 전처리기를 알아야 하나?
- #define 기본과 상수 매크로
- 조건부 컴파일 (#ifdef, #if, #ifndef)
- Include Guard 완벽 가이드
- 매크로 함수 (함수형 매크로)
- __FILE__와 LINE (위치 정보)
- Stringification과 Token Pasting
- 완전한 전처리기 예제
- 자주 발생하는 에러와 해결법
- 베스트 프랙티스
- 프로덕션 패턴
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.h를 main.cpp와 utils.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++) 호출 시 i나 j가 두 번 증가한다.
원인: 매크로는 텍스트 치환이라, 인자가 여러 번 평가될 수 있다.
해결: 인라인 함수·std::max 사용. 매크로를 쓸 수밖에 없다면 인자를 괄호로 감싸고, 전체를 괄호로 감싸는 등 주의해서 작성한다.
시나리오 7: 헤더 순환 의존
상황: A.h가 B.h를 include하고, B.h가 A.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::min이 std::(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<int와 int> 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
자주 쓰는 플랫폼 매크로:
| 매크로 | 의미 |
|---|---|
_WIN32 | Windows (32/64 공통) |
_WIN64 | Windows 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.cpp가 B.h와 C.h를 include하고, B.h와 C.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.h→CONFIG_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);
#cond는 stringification으로, 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.h에 APP_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.cpp 후 std::cout << VERSION이 1.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 100 후 std::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<int와 int>, x로 잘못 분리. typedef 또는 이중 괄호로 해결.
에러 11: LINE 문자열화
STRINGIFY(__LINE__)은 "__LINE__"이 됨. TOSTRING(__LINE__)처럼 2단계 매크로로 __LINE__을 먼저 확장한 뒤 stringify.
에러 12~15: 기타 주의사항
- 백슬래시 뒤 공백: 다중 줄 매크로에서
\직후 줄바꿈, 공백·주석 금지. - Include guard 충돌:
config.h와config_impl.h가 같은 guard를 쓰면 안 됨.CONFIG_H,CONFIG_IMPL_H처럼 구분. - #if 0 내부 중첩:
#ifdef가 있으면#endif매칭이 꼬일 수 있음.#ifdef NEVER_DEFINED또는 블록 주석 사용. - 매크로 재정의:
#ifndef PI/#define PI 3.14/#endif로 한 번만 정의.
에러 요약 표
| 에러 유형 | 원인 | 해결 |
|---|---|---|
| redefinition | Include guard 없음 | #ifndef/#define/#endif 추가 |
| 잘못된 매크로 확장 | 괄호 누락 | 인자·전체에 괄호 추가 |
| 인자 다중 평가 | 함수형 매크로 | 인라인 함수·std::max 사용 |
# 문법 오류 | #과 인자 사이 공백 | #x 형태로 붙여 쓰기 |
-D 문자열 전달 | 따옴표 누락 | -DVAR=\"value\" |
| 이름 충돌 | 매크로와 식별자 겹침 | 대문자·prefix 사용 |
| else 매칭 오류 | 다중 문장 매크로에 { } 사용 | do { } while(0) 사용 |
| 쉼표 인자 분리 | 매크로 인자에 템플릿 등 | typedef·이중 괄호 |
| LINE 문자열화 | 1단계 stringify | 2단계 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_LEVEL로 LOG_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_EXPORTS면 dllexport, 아니면 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면 -DNDEBUG를 add_definitions로 주입한다.
패턴 13: 단위 테스트 전용 매크로
#ifdef RUNNING_TESTS로 테스트 빌드에서만 TEST_ASSERT가 throw 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 기초)