C++ X-Macro 완벽 가이드 | enum-string 매핑·에러 코드·상태 머신·커맨드 테이블 실전
이 글의 핵심
C++ X-Macro 완벽 가이드에 대한 실전 가이드입니다. enum-string 매핑·에러 코드·상태 머신·커맨드 테이블 실전 등을 예제와 함께 상세히 설명합니다.
들어가며: “enum과 문자열을 두 번 정의하지 말고 싶어요”
구체적인 문제 시나리오
enum과 문자열, 에러 코드와 메시지, 상태와 전이 조건 등을 다룰 때 이런 상황을 자주 겪습니다:
- enum을 정의하고, 문자열 배열도 따로 만들고, switch로
to_string을 구현했는데, 새 값을 추가할 때 세 곳을 모두 수정해야 한다 - 에러 코드마다 숫자·문자열·HTTP 상태·재시도 가능 여부가 있는데, 한 곳만 빼먹으면 런타임에 이상 동작한다
- 상태 머신에서 상태 enum·전이 테이블·초기 상태를 각각 정의했더니, 상태 하나 추가할 때마다 여러 파일을 수정한다
- CLI 커맨드마다 이름·설명·핸들러 함수가 있는데, 명령어 추가 시 enum·help 텍스트·dispatch 테이블을 동기화해야 한다
이런 “동일한 데이터를 여러 형태로 반복 정의”하는 문제를 해결하는 기법이 X-Macro입니다. 한 곳에 데이터를 정의하고, 매크로를 재정의해 enum·문자열·switch·테이블 등을 자동 생성합니다.
X-Macro 동작 원리 시각화
flowchart TD
subgraph source["단일 소스 (데이터 정의)"]
D["X(RED)\nX(GREEN)\nX(BLUE)"]
end
subgraph gen1["매크로 X = enum 생성"]
E["enum RED, GREEN, BLUE"]
end
subgraph gen2["매크로 X = 문자열 배열"]
S["\"RED\", \"GREEN\", \"BLUE\""]
end
subgraph gen3["매크로 X = switch case"]
W["case RED: return \"RED\"; ..."]
end
D --> E
D --> S
D --> W
추가 문제 시나리오
시나리오 1: 로그 레벨
DEBUG, INFO, WARN, ERROR enum과 "DEBUG", "INFO" 문자열, 그리고 switch로 로그 레벨을 출력하는 함수가 있습니다. 새 레벨 TRACE를 추가하면 enum·문자열 배열·switch 세 곳을 수정해야 합니다.
시나리오 2: 프로토콜 메시지 타입
MSG_LOGIN, MSG_LOGOUT, MSG_PING 등 메시지 타입 enum과, 각 타입의 크기·직렬화 함수·역직렬화 함수가 있습니다. 새 메시지 추가 시 네 곳 이상을 동기화해야 합니다.
시나리오 3: 설정 키
config.h에 KEY_HOST, KEY_PORT, KEY_TIMEOUT 등 키 enum이 있고, config.cpp에 "host", "port", "timeout" 문자열과 파싱 로직이 있습니다. 키 추가 시 enum·문자열·파싱 분기·기본값을 모두 수정해야 합니다.
시나리오 4: 게임 몬스터 타입
몬스터마다 이름·아이콘·레벨·공격 타입·독 면역 여부가 있습니다. 배열로 관리하면 런타임에 조회할 수 있지만, X-Macro로 소스 코드에 인코딩하면 컴파일 타임에 switch로 전개되어 메모리 사용이 줄어듭니다.
시나리오 5: 파서 토큰
TOKEN_IF, TOKEN_ELSE, TOKEN_WHILE 등 토큰 enum과 "if", "else", "while" 키워드 문자열, 그리고 키워드→토큰 매핑 테이블이 있습니다. 새 키워드 추가 시 세 곳을 수정해야 합니다.
이 글을 읽으면
- X-Macro의 동작 원리와 “단일 소스 → 다중 생성” 패턴을 이해할 수 있습니다.
- enum-string 매핑, 에러 코드, 상태 머신, 커맨드 테이블 등 완전한 예제를 구현할 수 있습니다.
- 자주 발생하는 에러와 해결법을 알 수 있습니다.
- 프로덕션에서 바로 쓸 수 있는 패턴을 적용할 수 있습니다.
개념을 잡는 비유
템플릿 인자 자리는 붕어빵 틀의 칸 수가 정해지듯, 컴파일 시점에 크기·상수가 박혀 있어야 하는 경우가 많습니다. constexpr·컴파일 타임 계산은 그 값을 미리 찍어내어, 배열 크기와 static_assert 같은 곳에 그대로 얹을 수 있게 해 줍니다.
목차
- X-Macro 기초
- 예제 1: enum-string 매핑
- 예제 2: 에러 코드
- 예제 3: 상태 머신
- 예제 4: 커맨드 테이블
- 자주 발생하는 에러와 해결법
- 베스트 프랙티스
- 프로덕션 패턴
1. X-Macro 기초
1.1 X-Macro란?
X-Macro는 C/C++ 전처리기를 이용해 한 데이터 소스에서 여러 코드를 생성하는 기법입니다. 데이터를 X(...) 형태의 매크로 호출로 정의하고, #define X을 다르게 정의한 뒤 같은 데이터를 #include하면, 같은 데이터가 다른 형태로 전개됩니다.
이름 “X”는 관례적인 이름일 뿐, ITEM, ENTRY, DEF 등 어떤 이름이든 사용할 수 있습니다.
1.2 기본 구조
데이터는 별도 파일에 X(ID) 형태로만 나열합니다. X는 사용처에서 정의합니다:
// colors.def
X(RED)
X(GREEN)
X(BLUE)
// main.cpp - enum 생성
enum class Color {
#define X(ID) ID,
#include "colors.def"
#undef X
};
// 전개 결과:
// enum class Color {
// RED,
// GREEN,
// BLUE,
// };
// main.cpp - 문자열 배열 생성
const char* color_names[] = {
#define X(ID) #ID,
#include "colors.def"
#undef X
};
// 전개 결과:
// const char* color_names[] = {
// "RED",
// "GREEN",
// "BLUE",
// };
핵심: #define X(ID) ID,와 #define X(ID) #ID,로 **같은 colors.def**를 다른 형태로 전개합니다. #ID는 매크로 인자를 문자열 리터럴로 만드는 연산자입니다.
1.3 X-Macro 전개 흐름
flowchart LR
A["colors.def\nX(RED)\nX(GREEN)\nX(BLUE)"] --> B["#define X(ID) ID,"]
B --> C["RED,\nGREEN,\nBLUE,"]
A --> D["#define X(ID) #ID,"]
D --> E["\"RED\",\n\"GREEN\",\n\"BLUE\","]
1.4 최소 동작 예제
// colors.def
X(RED)
X(GREEN)
X(BLUE)
// main.cpp
#include <iostream>
enum class Color {
#define X(ID) ID,
#include "colors.def"
#undef X
};
const char* to_string(Color c) {
switch (c) {
#define X(ID) case Color::ID: return #ID;
#include "colors.def"
#undef X
default: return "unknown";
}
}
int main() {
std::cout << to_string(Color::GREEN) << "\n"; // GREEN
return 0;
}
설명: colors.def 한 파일만 수정하면 enum과 to_string이 자동으로 동기화됩니다. BLACK을 추가하려면 colors.def에 X(BLACK) 한 줄만 넣으면 됩니다.
2. 예제 1: enum-string 매핑
2.1 양방향 변환 (enum ↔ string)
enum을 문자열로, 문자열을 enum으로 변환하는 완전한 예제입니다.
// log_level.def
X(DEBUG)
X(INFO)
X(WARN)
X(ERROR)
// log_level.h
#pragma once
enum class LogLevel {
#define X(L) L,
#include "log_level.def"
#undef X
};
const char* to_string(LogLevel level);
LogLevel from_string(const char* s);
// log_level.cpp
#include "log_level.h"
#include <cstring>
const char* to_string(LogLevel level) {
switch (level) {
#define X(L) case LogLevel::L: return #L;
#include "log_level.def"
#undef X
default: return "UNKNOWN";
}
}
LogLevel from_string(const char* s) {
#define X(L) if (strcmp(s, #L) == 0) return LogLevel::L;
#include "log_level.def"
#undef X
// 파싱 실패 시 기본값 또는 예외
return LogLevel::INFO;
}
동작: to_string(LogLevel::WARN) → "WARN", from_string("ERROR") → LogLevel::ERROR.
2.2 enum 개수 자동 계산
// log_level.cpp - 개수 계산
constexpr size_t log_level_count = 0
#define X(L) +1
#include "log_level.def"
#undef X
;
// 전개: 0 +1 +1 +1 +1 = 4
주의: +1 앞에 공백이 있어야 0 +1로 파싱됩니다. 0+1도 동작하지만 가독성을 위해 공백을 넣습니다.
2.3 문자열 배열 (인덱스 접근)
// log_level.cpp
const char* const log_level_names[] = {
#define X(L) #L,
#include "log_level.def"
#undef X
};
// 사용: log_level_names[static_cast<size_t>(LogLevel::WARN)] == "WARN"
주의: enum 값이 0부터 연속적이어야 인덱스로 안전하게 접근할 수 있습니다. enum class는 기본적으로 0부터 시작하므로 문제없습니다.
2.4 사용자 정의 문자열 (enum과 다른 표시용 문자열)
enum 이름과 출력용 문자열이 다를 때는 인자를 두 개로 확장합니다.
// log_level.def - (enum이름, 표시문자열)
X(DEBUG, "Debug")
X(INFO, "Information")
X(WARN, "Warning")
X(ERROR, "Error")
// log_level.h
enum class LogLevel {
#define X(L, S) L,
#include "log_level.def"
#undef X
};
const char* to_string(LogLevel level);
// log_level.cpp
const char* to_string(LogLevel level) {
switch (level) {
#define X(L, S) case LogLevel::L: return S;
#include "log_level.def"
#undef X
default: return "Unknown";
}
}
// to_string(LogLevel::INFO) → "Information"
3. 예제 2: 에러 코드
3.1 에러 코드 + 숫자 + 메시지
에러 코드 enum, 숫자 값, 메시지 문자열을 한 소스에서 생성합니다.
// error_codes.def
// X(enum이름, 숫자값, 메시지)
X(SUCCESS, 0, "Success")
X(INVALID_ARG, -1, "Invalid argument")
X(NOT_FOUND, -2, "Resource not found")
X(TIMEOUT, -3, "Operation timed out")
X(IO_ERROR, -4, "I/O error")
// error_codes.h
#pragma once
enum class ErrorCode {
#define X(E, N, M) E = N,
#include "error_codes.def"
#undef X
};
const char* error_message(ErrorCode e);
int error_number(ErrorCode e);
// error_codes.cpp
#include "error_codes.h"
const char* error_message(ErrorCode e) {
switch (e) {
#define X(E, N, M) case ErrorCode::E: return M;
#include "error_codes.def"
#undef X
default: return "Unknown error";
}
}
int error_number(ErrorCode e) {
switch (e) {
#define X(E, N, M) case ErrorCode::E: return N;
#include "error_codes.def"
#undef X
default: return -999;
}
}
3.2 에러 코드 테이블 (배열로 조회)
런타임에 배열 인덱스로 조회하려면 enum 값을 0부터 연속으로 두는 것이 좋습니다. 숫자 값을 별도로 두려면 위와 같이 switch를 사용합니다.
// error_codes.def - 0부터 연속
X(SUCCESS, "Success")
X(INVALID_ARG, "Invalid argument")
X(NOT_FOUND, "Resource not found")
X(TIMEOUT, "Operation timed out")
// error_codes.cpp
struct ErrorInfo {
const char* message;
};
const ErrorInfo error_table[] = {
#define X(E, M) { M },
#include "error_codes.def"
#undef X
};
// error_table[static_cast<size_t>(ErrorCode::NOT_FOUND)].message
3.3 HTTP 상태 코드 매핑
// http_status.def
X(OK, 200, "OK")
X(CREATED, 201, "Created")
X(BAD_REQUEST, 400, "Bad Request")
X(NOT_FOUND, 404, "Not Found")
X(INTERNAL_ERROR, 500, "Internal Server Error")
// http_status.cpp
int http_status_code(HttpStatus s) {
switch (s) {
#define X(E, C, M) case HttpStatus::E: return C;
#include "http_status.def"
#undef X
default: return 500;
}
}
const char* http_status_message(HttpStatus s) {
switch (s) {
#define X(E, C, M) case HttpStatus::E: return M;
#include "http_status.def"
#undef X
default: return "Unknown";
}
}
4. 예제 3: 상태 머신
4.1 상태 정의와 전이 테이블
상태 enum, 초기 상태, 전이(transition) 조건을 X-Macro로 관리합니다.
// states.def
X(IDLE)
X(RUNNING)
X(PAUSED)
X(STOPPED)
// state_machine.h
#pragma once
enum class State {
#define X(S) S,
#include "states.def"
#undef X
};
const char* state_name(State s);
State initial_state(); // IDLE
// state_machine.cpp
#include "state_machine.h"
const char* state_name(State s) {
switch (s) {
#define X(S) case State::S: return #S;
#include "states.def"
#undef X
default: return "UNKNOWN";
}
}
State initial_state() {
return State::IDLE; // 첫 번째 상태를 초기값으로
}
4.2 전이 테이블 (이벤트 → 다음 상태)
이벤트와 현재 상태에 따른 다음 상태를 정의합니다.
// transitions.def
// X(현재상태, 이벤트, 다음상태)
X(IDLE, START, RUNNING)
X(RUNNING, PAUSE, PAUSED)
X(RUNNING, STOP, STOPPED)
X(PAUSED, RESUME, RUNNING)
X(PAUSED, STOP, STOPPED)
X(STOPPED, RESET, IDLE)
// state_machine.cpp
State transition(State current, Event ev) {
switch (current) {
#define X(FROM, EV, TO) case State::FROM: if (ev == Event::EV) return State::TO;
#include "transitions.def"
#undef X
default: return current;
}
}
주의: 위 패턴은 case 안에 if가 들어가므로, 한 상태에서 여러 이벤트를 처리하려면 switch를 중첩하거나, (FROM, EV, TO) 조합별로 분기하는 방식으로 확장해야 합니다. 아래는 더 실용적인 패턴입니다.
4.3 전이 테이블 (조합별 switch)
// transitions.def
X(IDLE, START, RUNNING)
X(RUNNING, PAUSE, PAUSED)
X(RUNNING, STOP, STOPPED)
X(PAUSED, RESUME, RUNNING)
X(PAUSED, STOP, STOPPED)
X(STOPPED, RESET, IDLE)
// 이벤트 enum
enum class Event { START, PAUSE, RESUME, STOP, RESET };
State transition(State current, Event ev) {
#define X(FROM, EV, TO) \
if (current == State::FROM && ev == Event::EV) return State::TO;
#include "transitions.def"
#undef X
return current; // 유효하지 않은 전이
}
4.4 상태 머신 다이어그램
stateDiagram-v2
[*] --> IDLE
IDLE --> RUNNING : START
RUNNING --> PAUSED : PAUSE
RUNNING --> STOPPED : STOP
PAUSED --> RUNNING : RESUME
PAUSED --> STOPPED : STOP
STOPPED --> IDLE : RESET
5. 예제 4: 커맨드 테이블
5.1 CLI 커맨드: 이름 + 설명 + 핸들러
커맨드 이름, 설명, 핸들러 함수를 한 소스에서 정의합니다.
// commands.def
// X(이름, 설명, 핸들러함수)
X(help, "Show help", cmd_help)
X(version, "Show version", cmd_version)
X(quit, "Exit program", cmd_quit)
X(config, "Show configuration", cmd_config)
// commands.h
#pragma once
#include <string>
#include <functional>
enum class CommandId {
#define X(N, D, H) N,
#include "commands.def"
#undef X
};
using CommandHandler = std::function<int(int argc, char** argv)>;
const char* command_name(CommandId id);
const char* command_description(CommandId id);
CommandHandler command_handler(CommandId id);
void print_all_commands(); // help 출력
// commands.cpp
#include "commands.h"
#include <iostream>
#include <cstring>
static int cmd_help(int, char**);
static int cmd_version(int, char**);
static int cmd_quit(int, char**);
static int cmd_config(int, char**);
const char* command_name(CommandId id) {
switch (id) {
#define X(N, D, H) case CommandId::N: return #N;
#include "commands.def"
#undef X
default: return "unknown";
}
}
const char* command_description(CommandId id) {
switch (id) {
#define X(N, D, H) case CommandId::N: return D;
#include "commands.def"
#undef X
default: return "";
}
}
CommandHandler command_handler(CommandId id) {
switch (id) {
#define X(N, D, H) case CommandId::N: return H;
#include "commands.def"
#undef X
default: return nullptr;
}
}
void print_all_commands() {
#define X(N, D, H) std::cout << " " #N " - " D "\n";
#include "commands.def"
#undef X
}
// 핸들러 구현
static int cmd_help(int, char**) {
std::cout << "Available commands:\n";
print_all_commands();
return 0;
}
static int cmd_version(int, char**) {
std::cout << "version 1.0\n";
return 0;
}
static int cmd_quit(int, char**) { return -1; } // -1 = 종료
static int cmd_config(int, char**) { /* ... */ return 0; }
5.2 문자열 → CommandId 파싱
CommandId parse_command(const char* s) {
#define X(N, D, H) if (strcmp(s, #N) == 0) return CommandId::N;
#include "commands.def"
#undef X
return static_cast<CommandId>(-1); // invalid
}
5.3 디스패치 루프 예시
int main(int argc, char** argv) {
if (argc < 2) {
cmd_help(0, nullptr);
return 0;
}
CommandId id = parse_command(argv[1]);
if (static_cast<int>(id) < 0) {
std::cerr << "Unknown command: " << argv[1] << "\n";
return 1;
}
auto handler = command_handler(id);
int ret = handler(argc - 2, argv + 2);
if (ret == -1) return 0; // quit
return ret;
}
6. 자주 발생하는 에러와 해결법
에러 1: 매크로 인자 개수 불일치
원인: colors.def가 X(RED) 한 개 인자인데, #define X(ID, NAME) NAME,처럼 두 개 인자를 기대하면 전처리 후 X(RED)가 RED,로만 전개되어 NAME이 비어 있거나 에러가 납니다.
// ❌ 잘못된 예: colors.def는 X(RED) 한 개 인자
// #define X(ID, NAME) NAME,
// #include "colors.def"
// → X(RED)가 NAME,로 전개되는데 NAME이 없음
해결: 데이터 정의와 매크로 정의의 인자 개수를 일치시킵니다.
// ✅ colors.def를 두 인자로 변경
X(RED, Red)
X(GREEN, Green)
X(BLUE, Blue)
#define X(ID, NAME) ID,
#include "colors.def"
#undef X
에러 2: #undef X 누락
원인: #include "colors.def" 후 #undef X를 하지 않으면, 이후 코드에서 X가 다른 의미로 사용될 때 충돌합니다.
// ❌ 잘못된 예
#define X(ID) ID,
#include "colors.def"
// #undef X 없음
int X = 42; // X가 매크로로 남아있어 에러
해결: #include 직후 반드시 #undef X를 호출합니다.
#define X(ID) ID,
#include "colors.def"
#undef X
에러 3: 쉼표·세미콜론 위치
원인: enum이나 배열 마지막 요소 뒤에 trailing comma가 있으면 C++에서는 허용되지만, 일부 구식 컴파일러나 C에서는 에러가 날 수 있습니다. 반대로 쉼표를 빼먹으면 구문 에러가 납니다.
// ❌ enum에서 쉼표 누락
#define X(ID) ID
#include "colors.def"
// → RED GREEN BLUE (쉼표 없음, 구문 에러)
// ✅ 올바른 예
#define X(ID) ID,
#include "colors.def"
#undef X
// → RED, GREEN, BLUE,
C++11 이후 enum의 trailing comma는 허용됩니다. C 호환성이 필요하면 X 정의를 조정해 마지막 요소에만 쉼표를 붙이지 않도록 할 수 있지만, 보통은 trailing comma를 사용하는 것이 단순합니다.
에러 4: .def 파일의 매크로 이름 충돌
원인: colors.def에서 X 대신 COLOR를 쓰고, 사용처에서 #define X(ID)를 하면 매칭되지 않습니다. .def 파일의 매크로 이름과 #define의 이름이 같아야 합니다.
// ❌ colors.def
COLOR(RED)
COLOR(GREEN)
// main.cpp
#define X(ID) ID,
#include "colors.def" // COLOR가 정의되지 않아서 COLOR(RED)가 그대로 남음
해결: .def 파일의 매크로 이름을 X로 통일하거나, 사용 전에 #define X COLOR로 별칭을 만듭니다.
// ✅ 방법 1: .def에서 X 사용
X(RED)
X(GREEN)
// ✅ 방법 2: COLOR를 X로 매핑
#define X(ID) ID,
#define COLOR X
#include "colors.def"
#undef COLOR
#undef X
에러 5: include 가드 없이 .def를 여러 번 include
원인: .def 파일에 #pragma once를 넣으면, 한 번 include된 후 재정의 없이 다시 include할 때 내용이 비어 있습니다. X-Macro는 같은 .def를 다른 X 정의로 여러 번 include하는 패턴이므로, .def에 include 가드를 넣으면 안 됩니다.
// ❌ colors.def
#pragma once
X(RED)
X(GREEN)
// main.cpp
#define X(ID) ID,
#include "colors.def" // OK
#undef X
#define X(ID) #ID,
#include "colors.def" // #pragma once 때문에 비어있음!
해결: .def 파일에는 include 가드를 넣지 않습니다. .def는 “데이터”일 뿐이고, 매번 include될 때마다 전개되어야 합니다.
에러 6: LIST_OF_ITEMS(X) 형태에서 매크로 확장 한도 초과
원인: #define LIST X(a)\nX(b)\n...처럼 한 매크로 안에 수백 개의 X(...)를 넣으면, 일부 컴파로는 매크로 확장 깊이/길이 제한에 걸립니다.
// ❌ 수백 개 항목을 한 매크로에
#define LIST_OF_COLORS(X) \
X(RED) X(GREEN) X(BLUE) ... (500개)
해결: .def 파일을 사용해 #include로 불러오면, 매크로 확장은 한 줄씩 일어나므로 제한에 덜 걸립니다. 항목이 매우 많으면(수천 개) X-Macro 대신 런타임 데이터 구조를 고려합니다.
에러 7: switch에 default 누락
원인: enum에 값을 추가했는데 to_string의 switch에 case를 추가하지 않으면, 새 값이 default로 빠집니다. X-Macro를 쓰면 .def만 수정하면 되므로 이 문제는 자동으로 해결됩니다. 다만 default를 두지 않으면 컴파일러가 “모든 case를 처리했는지” 경고할 수 있습니다.
// ✅ default로 unknown 처리
const char* to_string(Color c) {
switch (c) {
#define X(ID) case Color::ID: return #ID;
#include "colors.def"
#undef X
default: return "unknown";
}
}
에러 8: 문자열화 # 연산자와 인자
원인: #ID는 ID를 문자열 "ID"로 만듭니다. ID가 매크로로 확장된 결과를 문자열화하려면 #만으로는 부족하고, 이중 매크로가 필요할 수 있습니다. 단순히 X(RED)에서 #ID → "RED"는 문제없습니다.
#define X(ID) #ID
X(RED) // → "RED"
7. 베스트 프랙티스
1. .def 파일 네이밍
colors.def, error_codes.def, states.def처럼 데이터 도메인을 나타내는 이름을 사용합니다. .h가 아니라 .def 또는 .inc로 구분해 “헤더가 아니라 데이터/생성용”임을 드러냅니다.
2. X 대신 도메인별 이름 (선택)
가독성을 위해 #define COLOR(ID) ID,처럼 도메인별 매크로 이름을 쓰고, .def에서도 COLOR(RED)를 사용할 수 있습니다. 단, 한 파일에서 여러 X-Macro를 쓸 때는 X를 재사용하고 #undef로 정리하는 것이 단순합니다.
3. 인자 추가 시 영향 범위 문서화
매크로 인자를 추가하면(예: X(RED) → X(RED, 0xFF0000)) 모든 사용처에서 X 정의를 수정해야 합니다. colors.def 상단에 인자 스키마를 주석으로 남깁니다.
// colors.def
// X(enum_id, hex_color)
X(RED, 0xFF0000)
X(GREEN, 0x00FF00)
X(BLUE, 0x0000FF)
4. enum class 사용
C++11 이상에서는 enum class를 사용해 타입 안전성을 높입니다. Color::RED처럼 네임스페이스가 분리되어 int와의 암시적 변환도 막을 수 있습니다.
5. static_assert로 개수 검증
enum 개수와 배열 크기가 일치하는지 static_assert로 검증합니다.
const char* const names[] = {
#define X(ID) #ID,
#include "colors.def"
#undef X
};
enum class Color {
#define X(ID) ID,
#include "colors.def"
#undef X
};
static_assert(sizeof(names) / sizeof(names[0]) ==
static_cast<size_t>(Color::BLUE) - static_cast<size_t>(Color::RED) + 1,
"enum and names array size mismatch");
6. #undef는 블록 단위로
한 블록에서 여러 번 #include "colors.def"를 쓸 때마다 #define X → #include → #undef X를 쌍으로 유지합니다.
#define X(ID) ID,
#include "colors.def"
#undef X
#define X(ID) #ID,
#include "colors.def"
#undef X
8. 프로덕션 패턴
패턴 1: idempotent include (LIST_OF_X 매크로)
.def를 직접 include하지 않고, 매크로로 감싸서 “한 번 정의, 여러 번 사용”하게 할 수 있습니다. 이 방식은 매크로 확장 크기 제한에 유의해야 합니다.
// colors.inc
#define LIST_OF_COLORS(X) \
X(RED) \
X(GREEN) \
X(BLUE)
// main.cpp
#define X(ID) ID,
enum class Color { LIST_OF_COLORS(X) };
#undef X
#define X(ID) #ID,
const char* names[] = { LIST_OF_COLORS(X) };
#undef X
패턴 2: 프로토콜 메시지 타입
// messages.def
// X(타입, 크기, 직렬화함수)
X(PING, 4, serialize_ping)
X(PONG, 4, serialize_pong)
X(LOGIN, 64, serialize_login)
X(LOGOUT, 4, serialize_logout)
enum class MsgType {
#define X(T, SZ, F) T,
#include "messages.def"
#undef X
};
size_t message_size(MsgType t) {
switch (t) {
#define X(T, SZ, F) case MsgType::T: return SZ;
#include "messages.def"
#undef X
default: return 0;
}
}
패턴 3: 설정 키-기본값
// config.def
// X(키, 타입, 기본값문자열)
X(host, string, "localhost")
X(port, int, "8080")
X(timeout, int, "30")
const char* default_value(ConfigKey key) {
switch (key) {
#define X(K, T, D) case ConfigKey::K: return D;
#include "config.def"
#undef X
default: return "";
}
}
패턴 4: 테스트 케이스 등록
// test_cases.def
X(test_add)
X(test_sub)
X(test_mul)
void run_all_tests() {
#define X(T) T();
#include "test_cases.def"
#undef X
}
패턴 5: 플러그인/모듈 등록
// modules.def
X(ModuleA, init_a, shutdown_a)
X(ModuleB, init_b, shutdown_b)
void init_all() {
#define X(M, I, S) I();
#include "modules.def"
#undef X
}
void shutdown_all() {
#define X(M, I, S) S();
#include "modules.def"
#undef X
}
X-Macro 적용 체크리스트
실무에서 X-Macro를 도입할 때 확인할 항목입니다.
- 데이터 소스가 명확한가? (enum·문자열·숫자 등)
- 반복 정의가 2곳 이상인가? (DRY 위반)
- 인자 스키마를 문서화했는가?
- #undef X를 모든 include 직후에 호출하는가?
- .def 파일에 include 가드를 넣지 않았는가?
- 항목 수가 수천 개 이상이 아닌가? (컴파일 시간)
- enum class를 사용하는가?
- static_assert로 배열·enum 크기 일치를 검증하는가?
- 모든 코드 블록에 언어 태그(
cpp,mermaid)가 있는가?
정리
| 항목 | 내용 |
|---|---|
| X-Macro | 한 데이터 소스에서 매크로 재정의로 여러 코드 생성 |
| 핵심 | #define X(...) → #include "data.def" → #undef X |
| 장점 | DRY, enum·문자열·switch 동기화, 컴파일 타임 생성 |
| 단점 | 인자 변경 시 전 사용처 수정, 대량 데이터 시 컴파일 부담 |
| 적용 | enum-string, 에러 코드, 상태 머신, 커맨드 테이블 |
| 대안 | C++20 Reflection(미래), 런타임 테이블, 코드 생성기 |
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 가변 인자 템플릿 | Variadic Templates와 Fold Expression
- C++ Fold Expression 완벽 가이드 | 단항·이항·쉼표 fold·커스텀 연산자 실전
- C++ SFINAE 완벽 가이드 | enable_if·void_t
이 글에서 다루는 키워드 (관련 검색어)
C++ X-Macro, enum string mapping, 에러 코드 매크로, 상태 머신 코드 생성, DRY enum, 매크로 코드 생성, 전처리기 등으로 검색하시면 이 글이 도움이 됩니다.
한 줄 요약: X-Macro로 enum·문자열·에러 코드·상태 머신·커맨드 테이블을 한 소스에서 생성해 DRY를 지키고, 인자 변경 시 주의하며 #undef로 매크로를 정리합니다.
실전 체크리스트
실무에서 이 개념을 적용할 때 확인해야 할 사항입니다.
코드 작성 전
- 이 기법이 현재 문제를 해결하는 최선의 방법인가?
- 팀원들이 이 코드를 이해하고 유지보수할 수 있는가?
- 성능 요구사항을 만족하는가?
코드 작성 중
- 컴파일러 경고를 모두 해결했는가?
- 엣지 케이스를 고려했는가?
- 에러 처리가 적절한가?
코드 리뷰 시
- 코드의 의도가 명확한가?
- 테스트 케이스가 충분한가?
- 문서화가 되어 있는가?
이 체크리스트를 활용하여 실수를 줄이고 코드 품질을 높이세요.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. C++ X-Macro 기법으로 enum과 문자열·에러 코드·상태 머신·커맨드 테이블을 단일 소스에서 생성. 문제 시나리오, 완전한 예제, 자주 발생하는 에러, 베스트 프랙티스, 프로덕션 패턴. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
다음 글: C++ 시리즈 목차
관련 글
- C++ 시리즈 전체 보기
- C++ Adapter Pattern 완벽 가이드 | 인터페이스 변환과 호환성
- C++ ADL |
- C++ Aggregate Initialization |