C++ enum class: 밑바닥 타입, 비트 플래그, 문자열 변환 패턴

이 글의 핵심

enum class와 underlying type의 관계, 스코프·암시적 변환 규칙, 로그·직렬화용 문자열 변환 패턴, 비트마스크 플래그, 프로덕션에서의 ABI·경고·테스트까지 한 번에 정리합니다.

enum class란?

enum class (또는 scoped enum)는 C++11에서 도입된 타입 안전한 열거형입니다. 기존 enum의 문제점(암시적 변환, 이름 충돌)을 해결하며, 더 강력한 타입 안전성을 제공합니다.

// 일반 enum (C++03)
enum Color {
    RED,
    GREEN,
    BLUE
};

Color c = RED;  // OK
int x = RED;    // OK (암시적 변환)

// enum class (C++11)
enum class Status {
    SUCCESS,
    ERROR,
    PENDING
};

Status s = Status::SUCCESS;  // OK
// int x = Status::SUCCESS;  // 에러: 암시적 변환 불가
int x = static_cast<int>(Status::SUCCESS);  // 명시적 변환 필요

비스코프 enum의 암시적 변환이 실제로 문제를 일으키는 방식은 대개 “의도와 다른데 컴파일이 된다”는 것입니다. enum Color { RED }와 enum Fruit { APPLE }이 있을 때 if (color == APPLE)은 두 값이 모두 int로 승격되어 비교되므로 경고만 나거나 아무 말 없이 컴파일되고, setVolume(RED)처럼 정수를 받는 함수에 열거자를 넘겨도 통과합니다. enum class는 이런 코드를 컴파일 에러로 바꿔 줍니다. 또 비스코프 열거자는 둘러싼 스코프에 이름을 풀어 놓기 때문에, 헤더에 enum { OK, ERROR }를 선언하면 그 헤더를 include하는 모든 파일에서 OK라는 이름을 다른 용도로 쓸 수 없게 됩니다.

왜 필요한가?:

  • 타입 안전: 암시적 변환 방지
  • 스코프: 이름 충돌 방지
  • 명확성: Color::RED처럼 명시적 사용
  • 기본 타입 지정: 메모리 절약 가능
// ❌ 일반 enum: 이름 충돌
enum Color { RED, GREEN, BLUE };
enum Status { RED, GREEN };  // 에러: RED 중복

// ✅ enum class: 스코프로 구분
enum class Color { RED, GREEN, BLUE };
enum class Status { RED, GREEN };  // OK

enum vs enum class 비교:

특징enumenum class
암시적 변환✅ 가능❌ 불가
스코프❌ 없음✅ 있음
타입 안전❌ 약함✅ 강함
이름 충돌❌ 발생 가능✅ 방지
사용REDColor::RED

enum class의 장점:

// 1. 타입 안전
enum class Color { RED, GREEN, BLUE };
// int x = Color::RED;  // 에러: 암시적 변환 불가

// 2. 스코프
enum class Color { RED };
enum class Status { RED };  // OK: 다른 스코프

// 3. 명확성
Color c = Color::RED;  // 명시적

// 4. 기본 타입 지정
enum class SmallEnum : uint8_t { A, B, C };  // 1바이트

기본 사용법

enum class Color {
    RED,
    GREEN,
    BLUE
};

Color c = Color::RED;

if (c == Color::RED) {
    cout << "빨강" << endl;
}

// switch문
switch (c) {
    case Color::RED:
        cout << "빨강" << endl;
        break;
    case Color::GREEN:
        cout << "초록" << endl;
        break;
    case Color::BLUE:
        cout << "파랑" << endl;
        break;
}

switch에 default를 넣지 않은 것은 의도적입니다. 열거자를 모두 나열하고 default를 빼 두면, 나중에 Color에 YELLOW를 추가했을 때 GCC·Clang의 -Wswitch(-Wall에 포함)가 enumeration value 'YELLOW' not handled in switch 경고로 처리하지 않은 모든 switch를 찾아 줍니다. 반대로 습관적으로 default:를 넣으면 이 경고가 꺼져서, 새 값이 조용히 기본 분기로 흘러가는 버그가 됩니다. 아래 getMessage나 levelToString의 default가 바로 그런 예입니다. 값이 추가될 가능성이 있는 열거형이라면 default 없이 모든 케이스를 쓰고, 함수 끝에 도달할 수 없다는 표시(C++23 std::unreachable() 또는 로그 후 반환)를 switch 밖에 두는 편이 좋습니다.

명시적 값 지정

enum class HttpStatus {
    OK = 200,
    CREATED = 201,
    BAD_REQUEST = 400,
    UNAUTHORIZED = 401,
    NOT_FOUND = 404,
    SERVER_ERROR = 500
};

HttpStatus status = HttpStatus::OK;
int code = static_cast<int>(status);  // 200

HTTP 상태 코드처럼 값 자체에 의미가 있는 열거형은 반드시 숫자를 명시해야 합니다. 값을 생략하면 앞 열거자 + 1로 자동 매겨지므로, 중간에 열거자를 하나 끼워 넣는 순간 뒤의 모든 값이 밀립니다. 메모리 안에서만 쓰는 열거형이라면 상관없지만, DB 컬럼이나 파일·네트워크로 정수 값이 나가는 열거형이라면 이 한 줄의 수정이 기존 데이터를 모두 다른 의미로 바꿔 버립니다. 반대로 HttpStatus에 static_cast<HttpStatus>(418)처럼 선언되지 않은 값도 담을 수 있다는 점은 기억해 둘 만합니다. 열거형 타입은 “선언된 열거자 중 하나”를 보장하지 않습니다.

기본 타입 지정

// 기본: int
enum class Color : int {
    RED,
    GREEN,
    BLUE
};

// 작은 타입 사용 (메모리 절약)
enum class SmallEnum : uint8_t {
    A,
    B,
    C
};

// 큰 타입 사용
enum class BigEnum : uint64_t {
    LARGE_VALUE = 1000000000000
};

실전 예시

예시 1: 상태 머신

enum class State {
    IDLE,
    RUNNING,
    PAUSED,
    STOPPED
};

class StateMachine {
private:
    State currentState = State::IDLE;
    
public:
    void start() {
        if (currentState == State::IDLE) {
            currentState = State::RUNNING;
            cout << "시작" << endl;
        }
    }
    
    void pause() {
        if (currentState == State::RUNNING) {
            currentState = State::PAUSED;
            cout << "일시정지" << endl;
        }
    }
    
    void resume() {
        if (currentState == State::PAUSED) {
            currentState = State::RUNNING;
            cout << "재개" << endl;
        }
    }
    
    void stop() {
        currentState = State::STOPPED;
        cout << "정지" << endl;
    }
    
    State getState() const {
        return currentState;
    }
};

int main() {
    StateMachine sm;
    sm.start();
    sm.pause();
    sm.resume();
    sm.stop();
}

상태 머신 심화: 전이 테이블과 무효 전이

간단한 if 연쇄도 동작하지만, 상태가 늘어나면 전이 함수를 한곳에 모으는 편이 유지보수에 유리합니다. 아래는 (현재 상태, 이벤트) → 다음 상태를 표로 두는 스케치입니다. 실무에서는 std::optional<State>나 bool 반환으로 “이 전이는 허용되지 않음”을 표현합니다.

enum class Event { Start, Pause, Resume, Stop };

class StateMachine2 {
    State state_{State::IDLE};

    static bool tryTransition(State from, Event ev, State& out) {
        switch (from) {
        case State::IDLE:
            if (ev == Event::Start) { out = State::RUNNING; return true; }
            return false;
        case State::RUNNING:
            if (ev == Event::Pause) { out = State::PAUSED; return true; }
            if (ev == Event::Stop)  { out = State::STOPPED; return true; }
            return false;
        case State::PAUSED:
            if (ev == Event::Resume) { out = State::RUNNING; return true; }
            if (ev == Event::Stop)   { out = State::STOPPED; return true; }
            return false;
        case State::STOPPED:
            return false;
        }
        return false;
    }

public:
    bool dispatch(Event ev) {
        State next{};
        if (!tryTransition(state_, ev, next)) return false;
        state_ = next;
        return true;
    }
    State state() const { return state_; }
};

이 패턴은 로깅·테스트가 쉽으며, 나중에 전이 표를 데이터로 빼거나 그래프 도구로 시각화하기도 좋습니다.

예시 2: 에러 코드

enum class ErrorCode {
    SUCCESS = 0,
    FILE_NOT_FOUND,
    PERMISSION_DENIED,
    NETWORK_ERROR,
    INVALID_INPUT,
    UNKNOWN_ERROR
};

class Result {
private:
    ErrorCode code;
    string message;
    
public:
    Result(ErrorCode c, const string& msg = "") 
        : code(c), message(msg) {}
    
    bool isSuccess() const {
        return code == ErrorCode::SUCCESS;
    }
    
    ErrorCode getCode() const {
        return code;
    }
    
    string getMessage() const {
        switch (code) {
            case ErrorCode::SUCCESS:
                return "성공";
            case ErrorCode::FILE_NOT_FOUND:
                return "파일을 찾을 수 없음: " + message;
            case ErrorCode::PERMISSION_DENIED:
                return "권한 거부: " + message;
            case ErrorCode::NETWORK_ERROR:
                return "네트워크 오류: " + message;
            case ErrorCode::INVALID_INPUT:
                return "잘못된 입력: " + message;
            default:
                return "알 수 없는 오류";
        }
    }
};

Result readFile(const string& filename) {
    ifstream file(filename);
    if (!file) {
        return Result(ErrorCode::FILE_NOT_FOUND, filename);
    }
    
    // 파일 읽기
    return Result(ErrorCode::SUCCESS);
}

int main() {
    Result result = readFile("test.txt");
    
    if (result.isSuccess()) {
        cout << "성공!" << endl;
    } else {
        cout << "에러: " << result.getMessage() << endl;
    }
}

예시 3: 방향

enum class Direction {
    NORTH,
    EAST,
    SOUTH,
    WEST
};

class Player {
private:
    int x = 0, y = 0;
    Direction facing = Direction::NORTH;
    
public:
    void move(Direction dir) {
        switch (dir) {
            case Direction::NORTH:
                y++;
                break;
            case Direction::EAST:
                x++;
                break;
            case Direction::SOUTH:
                y--;
                break;
            case Direction::WEST:
                x--;
                break;
        }
        facing = dir;
    }
    
    void turnLeft() {
        facing = static_cast<Direction>(
            (static_cast<int>(facing) + 3) % 4
        );
    }
    
    void turnRight() {
        facing = static_cast<Direction>(
            (static_cast<int>(facing) + 1) % 4
        );
    }
    
    void print() const {
        cout << "위치: (" << x << ", " << y << ")" << endl;
        cout << "방향: ";
        switch (facing) {
            case Direction::NORTH: cout << "북"; break;
            case Direction::EAST:  cout << "동"; break;
            case Direction::SOUTH: cout << "남"; break;
            case Direction::WEST:  cout << "서"; break;
        }
        cout << endl;
    }
};

int main() {
    Player player;
    player.move(Direction::NORTH);
    player.move(Direction::NORTH);
    player.turnRight();
    player.move(Direction::EAST);
    player.print();
}

예시 4: 로그 레벨

enum class LogLevel {
    DEBUG,
    INFO,
    WARNING,
    ERROR,
    FATAL
};

class Logger {
private:
    LogLevel minLevel = LogLevel::INFO;
    
public:
    void setLevel(LogLevel level) {
        minLevel = level;
    }
    
    void log(LogLevel level, const string& message) {
        // 같은 enum class끼리는 <, >= 같은 관계 연산이 그대로 동작 (선언 순서 = 값 순서)
        if (level >= minLevel) {
            cout << "[" << levelToString(level) << "] " 
                 << message << endl;
        }
    }
    
    void debug(const string& msg) { log(LogLevel::DEBUG, msg); }
    void info(const string& msg) { log(LogLevel::INFO, msg); }
    void warning(const string& msg) { log(LogLevel::WARNING, msg); }
    void error(const string& msg) { log(LogLevel::ERROR, msg); }
    void fatal(const string& msg) { log(LogLevel::FATAL, msg); }
    
private:
    string levelToString(LogLevel level) const {
        switch (level) {
            case LogLevel::DEBUG:   return "DEBUG";
            case LogLevel::INFO:    return "INFO";
            case LogLevel::WARNING: return "WARNING";
            case LogLevel::ERROR:   return "ERROR";
            case LogLevel::FATAL:   return "FATAL";
            default:                return "UNKNOWN";
        }
    }
};

int main() {
    Logger logger;
    
    logger.debug("디버그 메시지");  // 출력 안 됨
    logger.info("정보 메시지");
    logger.warning("경고 메시지");
    logger.error("에러 메시지");
    
    logger.setLevel(LogLevel::DEBUG);
    logger.debug("이제 출력됨");
}

enum class가 막는 것은 다른 타입과의 암시적 변환이지, 같은 열거형끼리의 비교가 아닙니다. ==, !=뿐 아니라 <, >= 같은 관계 연산도 같은 enum class 값끼리는 밑바닥 정수 값 기준으로 동작하므로 위처럼 level >= minLevel을 바로 쓸 수 있습니다. 다만 이 비교는 선언된 정수 값의 순서에 의존하므로, 누군가 TRACE를 목록 끝에 추가하면 TRACE가 FATAL보다 “심각한” 레벨이 되어 버립니다. 순서에 의미가 있는 열거형이라면 값을 명시하고 주석으로 “순서 중요”를 남겨 두는 편이 안전합니다.

이 예제를 Windows에서 빌드하면 흔히 만나는 문제가 있습니다. <windows.h>가 포함된 번역 단위에서는 wingdi.h가 #define ERROR 0을 정의하므로 LogLevel::ERROR가 LogLevel::0으로 치환되어 syntax error: 'constant' 같은 이해하기 어려운 에러가 납니다. 빌드 설정에 따라 DEBUG도 매크로로 정의되어 있는 경우가 있습니다. enum class의 스코프는 전처리기 매크로에는 효과가 없으므로, 열거자 이름에 전부 대문자를 쓰지 않거나(LogLevel::Error) 헤더 순서를 조정하는 것이 해결책입니다. 열거자 이름을 kError나 Error처럼 짓는 코딩 스타일이 많은 이유이기도 합니다.

enum class 변환

enum class Color {
    RED,
    GREEN,
    BLUE
};

// enum class -> int
Color c = Color::RED;
int x = static_cast<int>(c);

// int -> enum class
int y = 1;
Color c2 = static_cast<Color>(y);

// 문자열 변환 (헬퍼 함수)
string toString(Color c) {
    switch (c) {
        case Color::RED:   return "RED";
        case Color::GREEN: return "GREEN";
        case Color::BLUE:  return "BLUE";
        default:           return "UNKNOWN";
    }
}

enum class와 밑바닥 타입(underlying type) — 내부 표현

enum class는 고유한 타입이지만, 저장·연산 시에는 밑바닥 정수 타입으로 구현됩니다. 밑바닥 타입을 쓰지 않은 enum class의 밑바닥 타입은 표준이 int로 고정합니다(비스코프 enum은 값 범위를 담을 수 있는 구현 정의 정수 타입). : uint8_t처럼 고정 밑바닥 타입을 주면 객체 크기·정렬·직렬화 바이트 폭이 명확해집니다.

std::underlying_type_t<E>와 static_cast는 “열거형 값 ↔ 정수”를 다룰 때의 표준 관용구입니다. 밑바닥 타입을 바꾸는 것은 ABI·저장 포맷에 직결되므로, 공유 라이브러리나 네트워크 프로토콜에 노출되는 열거형은 처음부터 고정하며, 값 추가 시에도 기존 숫자 값은 불변으로 두는 것이 안전합니다.

열거자 값이 밑바닥 타입이 표현할 수 있는 범위를 벗어나면 ill-formed입니다. static_cast로 선언되지 않은 정수를 넣는 경우는 규칙이 나뉩니다. 밑바닥 타입이 고정된 열거형(모든 enum class, : 타입을 쓴 enum)은 밑바닥 타입의 모든 값이 유효한 값이라 static_cast<Color>(7) 자체는 정의된 동작입니다. 반면 밑바닥 타입이 고정되지 않은 비스코프 enum에 값 범위 밖의 정수를 넣으면 C++17부터 미정의 동작입니다. 정의된 동작이라고 해서 안전한 것은 아닙니다. 모든 열거자를 처리한 switch는 7을 어느 케이스로도 보내지 않아 함수가 값을 반환하지 않고 끝나는 미정의 동작으로 이어질 수 있고, 최적화 컴파일러는 “열거자 외의 값은 없다”고 가정한 코드를 만들기도 합니다. 그래서 외부에서 들어오는 정수를 열거형으로 바꿀 때는 캐스팅 전에 범위를 검증하는 팀이 많습니다.

#include <cstdint>
#include <type_traits>

enum class Code : std::uint8_t { A = 1, B = 2 };

static_assert(sizeof(Code) == 1);
static_assert(std::is_same_v<std::underlying_type_t<Code>, std::uint8_t>);

constexpr std::uint8_t raw(Code c) {
    return static_cast<std::underlying_type_t<Code>>(c);
}

요약: enum class는 타입 안전성을 주고, 밑바닥 타입은 크기·ABI·직렬화를 고정하는 레버입니다. 둘은 자주 같이 설계합니다.

스코프형(enum class) vs 비스코프형(enum) — 규칙 정리

구분enum (비스코프)enum class (스코프)
열거자 이름바깥 스코프에 노출EnumName::Enumerator만 유효
정수로의 암시적 변환가능(정수 승격)불가 — static_cast 필요
다른 열거형과 혼동쉬움타입이 달라 혼동이 줄어듦
밑바닥 타입(미지정)구현 정의(보통 int까지 호환)C++11부터 enum class는 기본 int

전방 선언은 밑바닥 타입을 알려야 합니다. 예: enum class Color : std::uint8_t; — 이후 정의에서 같은 밑바닥 타입을 유지해야 합니다.

enum class Forward : int;  // 선언만

enum class Forward : int { X, Y };  // 정의

enum Old : short;  // C++11부터 비스코프 enum도 밑바닥 타입을 쓰면 전방 선언 가능

enum class는 밑바닥 타입을 생략해도 int로 정해져 있으므로 enum class Forward;만으로도 전방 선언할 수 있습니다. 전방 선언이 쓸모 있는 이유는 컴파일 의존성입니다. 헤더가 열거형을 함수 인자나 멤버 타입으로만 쓴다면 정의가 든 헤더를 include하지 않아도 되고, 열거자를 추가할 때마다 그 헤더를 포함한 수많은 파일이 다시 컴파일되는 일을 줄일 수 있습니다.

새 코드에서는 Scott Meyers의 Effective Modern C++ Item 10에서 권하듯 enum class를 기본값으로 두며, 레거시 C API와의 접점에서만 비스코프 enum을 최소 범위로 감싸는 편이 낫습니다.

문자열 변환 패턴 — 로그·직렬화·디버깅

C++ 표준에는 “열거형 ↔ 문자열” 반사가 없으므로, 단일 출처(single source of truth)를 만드는 패턴이 중요합니다.

  1. switch + exhaustiveness: 가장 흔함. 컴파일러가 -Wswitch / -Wswitch-enum으로 누락을 잡을 수 있음.
  2. X 매크로 / 매크로 테이블: 열거자 목록을 한 번만 정의하고 문자열·케이스를 동시 생성.
  3. constexpr 배열·std::array: 런타임 오버헤드를 줄이며, 문자열을 string_view로 노출.
  4. 서드파티: 빌드·라이선스 정책이 맞다면 magic_enum 등으로 반복을 줄임(팀 표준으로 고정).

역방향(문자열 → enum)은 실패 가능하므로 std::optional<E>나 bool+출력 인자 패턴을 씁니다. 외부 입력(설정 파일·JSON)에서는 대소문자·별칭까지 명세로 고정하세요.

#include <optional>
#include <string_view>

enum class Role { User, Admin };

constexpr std::optional<Role> parseRole(std::string_view s) {
    if (s == "User")  return Role::User;
    if (s == "Admin") return Role::Admin;
    return std::nullopt;
}

비트마스크·플래그 열거형

여러 비트를 한 정수에 묶을 때는 밑바닥 타입을 부호 없는 정수로 두며, 1u << k 형태로 열거자를 정의합니다. enum class는 기본적으로 비트 연산 타입이 아니므로, 프로젝트 전역에서 | & ~ ^ &= |=를 오버로드하거나, constexpr 자유 함수로 test/set/clear API를 제공합니다.

C++20 이후에는 필요 시 std::bitset·std::popcount 등과 조합해 가독성과 표준 알고리즘을 택할 수 있습니다. 중요한 규칙은 플래그용 열거형과 “서로 배타적인 상태”용 열거형을 섞지 않는 것입니다. 전자는 비트 OR로 합성되고, 후자는 한 번에 하나의 값만 가집니다.

#include <cstdint>
#include <type_traits>

enum class Perms : std::uint32_t {
    None = 0,
    Read = 1u << 0,
    Write = 1u << 1,
    Exec = 1u << 2,
};

constexpr Perms operator|(Perms a, Perms b) {
    using U = std::underlying_type_t<Perms>;
    return static_cast<Perms>(static_cast<U>(a) | static_cast<U>(b));
}
constexpr Perms operator&(Perms a, Perms b) {
    using U = std::underlying_type_t<Perms>;
    return static_cast<Perms>(static_cast<U>(a) & static_cast<U>(b));
}
constexpr bool any(Perms x) {
    using U = std::underlying_type_t<Perms>;
    return static_cast<U>(x) != 0;
}

프로덕션에서의 열거형 패턴

  • 저장·네트워크: 밑바닥 타입·열거자 정수 값을 스키마에 명시하며, 버전 간 값 추가는 끝에 append, 의미 변경 금지.
  • 컴파일 경고: -Wswitch-enum(또는 이에 상응)으로 switch 누락 방지; 불가능한 default는 std::unreachable()(C++23) 또는 팀 유틸로 표시.
  • 테스트: 직렬화된 정수·문자열과의 골든 케이스를 두어 리팩터 시 회귀를 잡음.
  • 바인딩: Python 등 다른 언어와 FFI 할 때는 정수 크기와 signedness를 문서화.
  • 리뷰 체크리스트: “이 열거형은 플래그인가 상태인가?”, “외부에서 오는 정수 캐스팅 검증은 있는가?”

이상의 규칙을 지키면 열거형이 타입 안전성과 운영 안정성을 동시에 만족시키기 쉽습니다.

자주 발생하는 문제

문제 1: 암시적 변환

enum class Color {
    RED,
    GREEN,
    BLUE
};

// ❌ 암시적 변환 불가
// int x = Color::RED;

// ✅ 명시적 변환
int x = static_cast<int>(Color::RED);

문제 2: 비교 연산

enum class Color {
    RED,
    GREEN,
    BLUE
};

// ❌ 다른 enum class와 비교 불가
enum class Status {
    OK,
    ERROR
};

// Color c = Color::RED;
// if (c == Status::OK) {}  // 에러

// ✅ 같은 enum class끼리만 비교
Color c = Color::RED;
if (c == Color::RED) {}  // OK

문제 3: 스코프

// ❌ 스코프 없이 사용 불가
enum class Color {
    RED,
    GREEN,
    BLUE
};

// Color c = RED;  // 에러

// ✅ 스코프 필수
Color c = Color::RED;

실무 패턴

앞의 「비트마스크·플래그 열거형」 절에서 설계 원칙을 정리했으며, 아래는 매크로·순회 등 바로 옮겨 쓰기 좋은 스니펫입니다.

패턴 1: 비트 플래그

enum class는 기본적으로 비트 연산을 지원하지 않으므로, 플래그 전용 타입이라면 | & ~ ^를 자유롭게 쓰려면 아래처럼 연산자 오버로딩과 constexpr 헬퍼를 함께 두는 경우가 많습니다. std::underlying_type_t<E>로 캐스팅하면 의도가 드러납니다.

template<class E>
constexpr auto to_underlying(E e) noexcept {
    return static_cast<std::underlying_type_t<E>>(e);
}

C++23부터는 이 함수가 std::to_underlying(<utility>)으로 표준에 들어왔으므로, 최신 컴파일러에서는 직접 만들 필요가 없습니다. 직접 정의한 to_underlying을 전역 이름으로 두면 C++23으로 올릴 때 ADL로 표준 버전과 충돌할 수 있으니 프로젝트 네임스페이스 안에 두는 편이 좋습니다.

아래 예제에서 연산자 오버로드는 열거형과 같은 네임스페이스에 정의해야 합니다. 다른 네임스페이스에 두면 ADL(인자 의존 탐색)로 찾을 수 없어, 다른 네임스페이스에서 Permission::READ | Permission::WRITE를 쓰는 순간 no match for 'operator|' 에러가 납니다. 또 |만 정의하고 |=, ~를 빠뜨리면 perms |= Permission::EXECUTE;나 비트 제거(perms & ~Permission::WRITE)에서 같은 에러를 만나므로, 플래그 열거형이라면 여섯 개 연산자를 한 번에 정의하는 매크로나 템플릿을 두는 경우가 많습니다.

비트 플래그 예시 (기존 패턴 보강)

enum class Permission : uint32_t {
    NONE = 0,
    READ = 1 << 0,   // 1
    WRITE = 1 << 1,  // 2
    EXECUTE = 1 << 2 // 4
};

// 비트 연산자 오버로딩
Permission operator|(Permission lhs, Permission rhs) {
    return static_cast<Permission>(
        static_cast<uint32_t>(lhs) | static_cast<uint32_t>(rhs)
    );
}

Permission operator&(Permission lhs, Permission rhs) {
    return static_cast<Permission>(
        static_cast<uint32_t>(lhs) & static_cast<uint32_t>(rhs)
    );
}

bool hasPermission(Permission flags, Permission check) {
    return (flags & check) == check;
}

// 사용
Permission userPerms = Permission::READ | Permission::WRITE;
if (hasPermission(userPerms, Permission::READ)) {
    std::cout << "읽기 권한 있음\n";
}

enum을 쓰면 암시적으로 정수로 승격되어 실수로 +만 해도 플래그가 꼬일 수 있습니다. enum class는 정수와 섞이지 않게 막아 주므로, 권한·OpenGL/Vulkan 스타일 비트마스크·파일 open 플래그 등에 적합합니다. 단위 테스트에서는 모든 비트 조합을 다 돌리기보다 대표적인 조합과 경계(0, all bits) 위주로 검증하면 됩니다.

패턴 2: 문자열 변환 (매크로)

#define ENUM_TO_STRING(EnumType) \
    inline const char* toString(EnumType value) { \
        switch (value) {

#define ENUM_CASE(EnumValue) \
    case EnumValue: return #EnumValue;

#define END_ENUM_TO_STRING \
            default: return "UNKNOWN"; \
        } \
    }

enum class Color { RED, GREEN, BLUE };

ENUM_TO_STRING(Color)
    ENUM_CASE(Color::RED)
    ENUM_CASE(Color::GREEN)
    ENUM_CASE(Color::BLUE)
END_ENUM_TO_STRING

// 사용
std::cout << toString(Color::RED) << '\n';  // "Color::RED"

이 매크로는 #EnumValue로 인자를 토큰 그대로 문자열로 만들기 때문에, 결과가 "RED"가 아니라 "Color::RED"가 됩니다. 로그용으로는 괜찮지만 설정 파일이나 JSON에 쓰는 이름과 맞추려면 접두사를 잘라 내야 합니다. 더 근본적인 약점은 열거형 정의와 ENUM_CASE 목록이 따로 있다는 점입니다. 열거자를 추가하고 목록 갱신을 잊으면 default가 “UNKNOWN”을 돌려주고, default가 있어서 -Wswitch 경고도 나오지 않습니다. 열거자 목록 자체를 매크로 하나에 한 번만 적고 그것으로 열거형 정의와 문자열 테이블을 동시에 생성하는 X 매크로 방식이 “단일 출처” 문제를 제대로 푸는 방법입니다. magic_enum 같은 라이브러리는 컴파일러의 __PRETTY_FUNCTION__ 출력을 파싱하는 방식이라 편리하지만, 기본적으로 -128~127 범위의 값만 다루는 등 제약이 있으니 비트 플래그나 HTTP 코드처럼 큰 값에는 설정을 확인해야 합니다.

패턴 3: 범위 기반 순회

enum class Color { RED, GREEN, BLUE, COUNT };

template<typename Enum>
class EnumIterator {
    using value_type = Enum;
    value_type value_;
    
public:
    EnumIterator(value_type value) : value_(value) {}
    
    EnumIterator& operator++() {
        value_ = static_cast<Enum>(static_cast<int>(value_) + 1);
        return *this;
    }
    
    bool operator!=(const EnumIterator& other) const {
        return value_ != other.value_;
    }
    
    Enum operator*() const {
        return value_;
    }
};

template<typename Enum>
class EnumRange {
    Enum begin_;
    Enum end_;
    
public:
    EnumRange(Enum begin, Enum end) : begin_(begin), end_(end) {}
    
    EnumIterator<Enum> begin() const {
        return EnumIterator<Enum>(begin_);
    }
    
    EnumIterator<Enum> end() const {
        return EnumIterator<Enum>(end_);
    }
};

// 사용
for (Color c : EnumRange(Color::RED, Color::COUNT)) {
    std::cout << static_cast<int>(c) << '\n';
}

이 순회는 열거자 값이 0부터 빈틈없이 연속이라는 전제에서만 맞습니다. HttpStatus처럼 값이 띄엄띄엄 있는 열거형에 쓰면 201과 400 사이의 선언되지 않은 값까지 모두 순회합니다. COUNT 같은 센티널 열거자도 트레이드오프가 있습니다. 개수를 얻기 편하고 배열 크기로 쓰기 좋지만, switch에서 COUNT를 처리하지 않으면 -Wswitch 경고가 나고, API 사용자가 Color::COUNT를 실제 색으로 넘길 수도 있습니다. 순회가 필요한 열거형이 몇 개 안 된다면 아래 FAQ Q6의 방법 1처럼 constexpr std::array에 열거자를 나열하는 쪽이 가장 단순하고 틀릴 여지가 적습니다.

FAQ

Q1: enum vs enum class?

A:

  • enum: 암시적 변환 가능, 스코프 없음, 이름 충돌 가능
  • enum class: 타입 안전, 스코프 있음, 명시적 변환 필요
// enum: 암시적 변환
enum Color { RED };
int x = RED;  // OK

// enum class: 명시적 변환
enum class Status { RED };
// int x = Status::RED;  // 에러
int x = static_cast<int>(Status::RED);  // OK

권장: 새 코드에서는 enum class 사용

Q2: enum class는 언제 사용해야 하나요?

A:

  • 타입 안전성이 필요할 때: 암시적 변환 방지
  • 이름 충돌을 방지하고 싶을 때: 스코프 제공
  • 명확한 코드를 원할 때: Color::RED처럼 명시적
// 타입 안전
enum class Color { RED };
// if (Color::RED == 0) {}  // 에러

// 이름 충돌 방지
enum class Color { RED };
enum class Status { RED };  // OK

// 명확성
Color c = Color::RED;  // 명시적

Q3: 기본 타입 지정은 어떻게 하나요?

A: : type 문법을 사용합니다. 메모리 절약이나 큰 값 저장에 유용합니다.

// 작은 타입 (메모리 절약)
enum class SmallEnum : uint8_t {
    A, B, C
};  // 1바이트

// 큰 타입 (큰 값 저장)
enum class BigEnum : uint64_t {
    LARGE_VALUE = 1000000000000
};  // 8바이트

// 기본: int (4바이트)
enum class DefaultEnum {
    A, B, C
};

Q4: enum class를 문자열로 변환하려면?

A: switch문이나 map으로 직접 구현해야 합니다. C++에는 내장 기능이 없습니다.

enum class Color { RED, GREEN, BLUE };

// switch문 사용
std::string toString(Color c) {
    switch (c) {
        case Color::RED:   return "RED";
        case Color::GREEN: return "GREEN";
        case Color::BLUE:  return "BLUE";
        default:           return "UNKNOWN";
    }
}

// map 사용
std::map<Color, std::string> colorNames = {
    {Color::RED, "RED"},
    {Color::GREEN, "GREEN"},
    {Color::BLUE, "BLUE"}
};

Q5: enum class로 비트 플래그를 사용할 수 있나요?

A: 가능하지만 비트 연산자를 오버로딩해야 합니다.

enum class Flags : uint32_t {
    NONE = 0,
    FLAG_A = 1 << 0,
    FLAG_B = 1 << 1,
    FLAG_C = 1 << 2
};

Flags operator|(Flags lhs, Flags rhs) {
    return static_cast<Flags>(
        static_cast<uint32_t>(lhs) | static_cast<uint32_t>(rhs)
    );
}

// 사용
Flags flags = Flags::FLAG_A | Flags::FLAG_B;

Q6: enum class의 값을 순회하려면?

A: 범위 기반 for를 직접 구현하거나, 값을 배열에 저장하여 순회합니다.

enum class Color { RED, GREEN, BLUE, COUNT };

// 방법 1: 배열 사용
std::array<Color, 3> colors = {
    Color::RED, Color::GREEN, Color::BLUE
};

for (Color c : colors) {
    // ...
}

// 방법 2: 범위 기반 순회 구현 (위 "실무 패턴 3" 참조)

Q7: enum class 학습 리소스는?

A:

관련 글: Bit Manipulation.

enum class는 타입 안전하고 스코프가 있는 C++11 열거형입니다.


같이 보면 좋은 글