C++ X-Macro 완벽 가이드 | enum-string 매핑·에러 코드·상태 머신·커맨드 테이블 실전

C++ X-Macro 완벽 가이드 | enum-string 매핑·에러 코드·상태 머신·커맨드 테이블 실전

이 글의 핵심

C++ X-Macro 완벽 가이드에 대한 실전 가이드입니다. enum-string 매핑·에러 코드·상태 머신·커맨드 테이블 실전 등을 예제와 함께 상세히 설명합니다.

들어가며: “enum과 문자열을 두 번 정의하지 말고 싶어요”

구체적인 문제 시나리오

enum과 문자열, 에러 코드와 메시지, 상태와 전이 조건 등을 다룰 때 이런 상황을 자주 겪습니다:

  • enum을 정의하고, 문자열 배열도 따로 만들고, switchto_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.hKEY_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 같은 곳에 그대로 얹을 수 있게 해 줍니다.


목차

  1. X-Macro 기초
  2. 예제 1: enum-string 매핑
  3. 예제 2: 에러 코드
  4. 예제 3: 상태 머신
  5. 예제 4: 커맨드 테이블
  6. 자주 발생하는 에러와 해결법
  7. 베스트 프랙티스
  8. 프로덕션 패턴

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.defX(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.defX(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: 문자열화 # 연산자와 인자

원인: #IDID를 문자열 "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 |