Type-safe Flags 패턴 — Enum Class·Strong Typedef·Tag
#한 줄 요약
“
enum class위에operator|만 정의하면 type-safe bit flag가 됩니다.” 정수 변환을 차단하고 의도를 명확히 드러냅니다.
#어떤 문제를 푸는가
전통적인 C bit flag는 다음과 같이 씁니다.
#define FLAG_READ (1 << 0)#define FLAG_WRITE (1 << 1)#define FLAG_EXECUTE (1 << 2)#define FLAG_USER (1 << 3)
int permissions = FLAG_READ | FLAG_WRITE;
// 문제int speed = 42;permissions |= speed; // 무관한 정수 OR — 의미 없음, 컴파일 통과flag와 일반 정수가 같은 type이므로 섞이는 실수를 컴파일러가 잡지 못합니다.
C++의 enum class와 연산자 정의를 함께 쓰면 type-safe가 됩니다.
enum class Permission : uint32_t { Read = 1 << 0, Write = 1 << 1, Execute = 1 << 2, User = 1 << 3,};
constexpr Permission operator|(Permission a, Permission b) { return static_cast<Permission>( static_cast<uint32_t>(a) | static_cast<uint32_t>(b));}
Permission p = Permission::Read | Permission::Write; // OKint speed = 42;p |= speed; // ERROR — type mismatch정수와 섞이지 않으며 의도가 명확히 드러납니다.
#기본 패턴
enum class Flag : uint32_t { None = 0, Option1 = 1 << 0, Option2 = 1 << 1, Option3 = 1 << 2, All = Option1 | Option2 | Option3,};
// 비트 ORconstexpr Flag operator|(Flag a, Flag b) noexcept { return static_cast<Flag>( static_cast<uint32_t>(a) | static_cast<uint32_t>(b));}
// 비트 ANDconstexpr Flag operator&(Flag a, Flag b) noexcept { return static_cast<Flag>( static_cast<uint32_t>(a) & static_cast<uint32_t>(b));}
// 비트 NOTconstexpr Flag operator~(Flag a) noexcept { return static_cast<Flag>(~static_cast<uint32_t>(a));}
// 비트 XORconstexpr Flag operator^(Flag a, Flag b) noexcept { return static_cast<Flag>( static_cast<uint32_t>(a) ^ static_cast<uint32_t>(b));}
// 복합 연산자constexpr Flag& operator|=(Flag& a, Flag b) noexcept { a = a | b; return a; }constexpr Flag& operator&=(Flag& a, Flag b) noexcept { a = a & b; return a; }constexpr Flag& operator^=(Flag& a, Flag b) noexcept { a = a ^ b; return a; }
// bool 변환constexpr bool has_flag(Flag a, Flag b) noexcept { return (static_cast<uint32_t>(a) & static_cast<uint32_t>(b)) == static_cast<uint32_t>(b);}연산자는 namespace 안이나 enum과 같은 scope에 두어 ADL로 찾도록 합니다.
#사용 예
Flag p = Flag::Option1 | Flag::Option2;
if (has_flag(p, Flag::Option1)) { // Option1 있음}
p |= Flag::Option3; // 추가p &= ~Flag::Option1; // 제거p ^= Flag::Option2; // toggleC++ idiomatic한 형태입니다.
#Template으로 자동화
enum마다 operator를 정의하기는 번거롭습니다. 매크로나 trait로 자동화합니다.
#Trait + concept (C++20)
template<typename E>struct enable_bit_flags : std::false_type {};
template<typename E>constexpr bool enable_bit_flags_v = enable_bit_flags<E>::value;
template<typename E>concept BitFlags = std::is_enum_v<E> && enable_bit_flags_v<E>;
// 한 번만 정의template<BitFlags E>constexpr E operator|(E a, E b) noexcept { using U = std::underlying_type_t<E>; return static_cast<E>(static_cast<U>(a) | static_cast<U>(b));}
template<BitFlags E>constexpr E operator&(E a, E b) noexcept { using U = std::underlying_type_t<E>; return static_cast<E>(static_cast<U>(a) & static_cast<U>(b));}
// ... operator~, ^, |=, &=, ^=
// 사용enum class Permission : uint32_t { Read = 1, Write = 2, Execute = 4 };
template<>struct enable_bit_flags<Permission> : std::true_type {};
// 이제 자동Permission p = Permission::Read | Permission::Write;새 enum을 추가할 때 enable_bit_flags<E> 특수화 한 줄만 더 쓰면 나머지는 자동입니다.
#Macro 버전 (간단)
#define DEFINE_BIT_FLAGS(E) \ constexpr E operator|(E a, E b) noexcept { \ using U = std::underlying_type_t<E>; \ return static_cast<E>(static_cast<U>(a) | static_cast<U>(b)); \ } \ constexpr E operator&(E a, E b) noexcept { \ using U = std::underlying_type_t<E>; \ return static_cast<E>(static_cast<U>(a) & static_cast<U>(b)); \ } \ constexpr E operator~(E a) noexcept { \ using U = std::underlying_type_t<E>; \ return static_cast<E>(~static_cast<U>(a)); \ } \ constexpr E& operator|=(E& a, E b) noexcept { a = a | b; return a; } \ constexpr E& operator&=(E& a, E b) noexcept { a = a & b; return a; } \ /* ... */
enum class Permission : uint32_t { Read = 1, Write = 2 };DEFINE_BIT_FLAGS(Permission)매크로는 C++17 이전 환경에 잘 맞고, C++20 이상에서는 template을 선호합니다.
#임베디드 — Register Bit Flags
ARM Cortex의 register flag를 type-safe하게 다룰 수 있습니다.
enum class GpioConfig : uint32_t { None = 0, InputPullUp = 0b001, InputPullDown = 0b010, Output = 0b011, AlternateFn = 0b100, AnalogMode = 0b111, HighSpeed = 1 << 4, OpenDrain = 1 << 5,};
template<>struct enable_bit_flags<GpioConfig> : std::true_type {};
void configure_gpio(int pin, GpioConfig cfg) { uint32_t reg = GPIOA->MODER; reg &= ~(0b111 << (pin * 2)); reg |= static_cast<uint32_t>(cfg & GpioConfig::AnalogMode) << (pin * 2); GPIOA->MODER = reg;
if (has_flag(cfg, GpioConfig::HighSpeed)) { GPIOA->OSPEEDR |= 1 << (pin * 2); } if (has_flag(cfg, GpioConfig::OpenDrain)) { GPIOA->OTYPER |= 1 << pin; }}
configure_gpio(5, GpioConfig::Output | GpioConfig::HighSpeed);#define MODE_OUTPUT 매크로보다 type-safe하며 디버거에서 enum 이름을 그대로 확인할 수 있습니다.
#임베디드 — Status Register Flags
enum class UartStatus : uint32_t { None = 0, DataReady = 1 << 0, Overrun = 1 << 1, NoiseDetected = 1 << 2, FramingError = 1 << 3, ParityError = 1 << 4, AnyError = Overrun | NoiseDetected | FramingError | ParityError,};
template<>struct enable_bit_flags<UartStatus> : std::true_type {};
UartStatus read_status() { return static_cast<UartStatus>(USART2->SR);}
void uart_isr() { auto status = read_status();
if (has_flag(status, UartStatus::DataReady)) { read_data(); } if (has_flag(status, UartStatus::AnyError)) { log_error("UART error"); clear_errors(); }}ISR에서 상태 검사가 명확해집니다.
#To-string for logging
flag 값을 문자열로 출력할 때도 활용합니다.
constexpr const char* to_string(GpioConfig cfg) { // 단일 값은 단순 switch (cfg) { case GpioConfig::None: return "None"; case GpioConfig::Output: return "Output"; // ... } return "Combined"; // 여러 비트}
// 또는 combined 처리void format_flags(GpioConfig cfg, char* buf, size_t n) { buf[0] = 0; if (has_flag(cfg, GpioConfig::HighSpeed)) strncat(buf, "HighSpeed|", n); if (has_flag(cfg, GpioConfig::OpenDrain)) strncat(buf, "OpenDrain|", n); // ... // 마지막 | 제거 size_t len = strlen(buf); if (len > 0 && buf[len - 1] == '|') buf[len - 1] = 0;}C++20 이후 reflection이 추가되면 자동 to_string이 가능해집니다. 현재는 수동으로 작성합니다.
#Strongly-typed Flags
C++의 std::bitset처럼 wrapper class로 감싸는 방식입니다.
template<typename E>class Flags { using U = std::underlying_type_t<E>; U value_;
public: constexpr Flags() : value_(0) {} constexpr Flags(E e) : value_(static_cast<U>(e)) {} constexpr Flags(U v) : value_(v) {}
constexpr Flags operator|(Flags other) const { return value_ | other.value_; } constexpr Flags operator&(Flags other) const { return value_ & other.value_; } constexpr Flags operator~() const { return ~value_; }
constexpr bool has(E flag) const { return (value_ & static_cast<U>(flag)) == static_cast<U>(flag); }
constexpr U value() const { return value_; }};
// 사용Flags<Permission> p;p = Permission::Read;p |= Permission::Write;
if (p.has(Permission::Read)) { /* */ }한 번의 operator 정의로 모두 처리할 수 있지만 enum을 직접 쓰는 것보다 약간 무겁습니다.
#자주 보는 함정과 안티패턴
#1. enum class 없는 enum
enum Permission { Read = 1, Write = 2 }; // 옛 enumint x = Read; // 암묵 변환항상 enum class를 사용하고, 옛 enum은 legacy 호환 용도로만 둡니다.
#2. operator 정의 누락
enum class Flag : uint32_t { A = 1, B = 2 };auto x = Flag::A | Flag::B; // ERROR — operator| 없음매크로나 template으로 자동화합니다.
#3. underlying type 명시 안 함
enum class Flag { A = 1 << 30 }; // int 가정 — 32-bit 한계항상 : uint32_t 같은 underlying type을 명시합니다.
#4. bit position 실수
enum class Flag { A = 1, B = 2, C = 3 }; // C는 A|B와 같음 — flag 아님A = 1 << 0, B = 1 << 1, C = 1 << 2처럼 비트 위치를 명확히 합니다.
#5. enum value 충돌
enum class A { X = 1 };enum class B { X = 2 };A::X | B::X; // type 다름 — 컴파일 에러 (의도된 안전)같은 enum의 flag끼리만 조합합니다.
#6. Strongly typed flags의 overhead 가정
enum class + operator는 zero-cost이며 컴파일러가 모두 인라인합니다.
#측정 — type-safe vs raw
// V1 — raw intint flags = (1 << 0) | (1 << 1);
// V2 — enum class with operatorsauto flags = MyFlag::A | MyFlag::B;어셈블리:
V1: mov r0, #3 ; 1 | 2 = 3
V2: mov r0, #3 ; 동일생성된 코드가 완전히 동일하며 zero-cost입니다.
#정리
- C의
#define FLAG_X (1<<n)대신enum class와 bit operator를 함께 씁니다. - 무관한 정수와 섞이지 않으므로 type safety가 보장됩니다.
- Template이나 매크로로 operator를 자동 정의합니다.
- 임베디드의 register flag와 status flag에 적합합니다.
- 컴파일 결과가 동일하므로 zero-cost입니다.
#관련 항목
- Part 2-09: Type Traits — concept 활용
- Part 5-01: Register 추상화 — type-safe register
- Part 5-02: GPIO 추상화
#다음 글
Part 4-06: State Machine — 상태와 전이를 type-safe하게. enum + switch부터 std::variant까지.
Embedded C++ for Real Systems · 34 of 41
- 1Embedded C++ for Real Systems — 임베디드 모던 C++ 시리즈 소개
- 2임베디드 C++ vs C — 런타임·코드 크기·ABI 관점 비교
- 3임베디드 C++ 컴파일러 플래그 분석 — -fno-rtti·-fno-exceptions·-Os
- 4임베디드 C++ 런타임 요구사항 — libstdc++·newlib·crt0 분석
- 5C++ 코드 크기 분석 — 가상 함수·템플릿·예외 비용 추적
- 6C++ ABI 호환성 — Itanium ABI·name mangling·vtable 레이아웃
- 7C++ 스타트업 코드 분석 — .init_array·전역 생성자 호출 순서
- 8임베디드 C++ 링커 스크립트 — vtable·정적 객체 배치 추적
- 9임베디드 C++ 표준 선택 가이드 — C++11/14/17/20/23 트레이드오프
- 10임베디드 RAII 기초 — 리소스 안전성과 결정적 소멸 보장
- 11임베디드 RAII 실전 패턴 — Lock·Pin·DMA·Power 관리
- 12constexpr 기초와 임베디드 적용 — 컴파일 타임 계산 활용
- 13constexpr 고급 활용 — 룩업 테이블·CRC·해시 컴파일 타임 생성
- 14consteval과 constinit 분석 — C++20 컴파일 타임 강제 메커니즘
- 15임베디드 Templates 기초 — 타입 안전과 코드 재사용 분석
- 16Template 비용 분석 — 코드 폭증·인스턴스화·디버그 정보 측정
- 17CRTP 패턴 분석 — vtable 없는 정적 다형성
- 18Type Traits 임베디드 활용 — SFINAE·is_pod·컴파일 타임 검사
- 19C++20 Concepts 활용 — 템플릿 제약과 가독성 개선
- 20동적 할당 없는 임베디드 C++ — placement new·정적 객체·풀
- 21Custom Allocator 기초 — std::allocator 인터페이스 분석
- 22Pool Allocator 구현 — Fixed-Size Block과 O(1) 보장
- 23std::pmr 임베디드 활용 — Polymorphic Memory Resource 분석
- 24No-Exception C++ 설계 — 코드 크기·결정성 트레이드오프
- 25임베디드 에러 처리 패턴 — Result·errno·optional 비교
- 26std::expected 분석 — C++23 결과 타입과 에러 전파
- 27No-RTTI C++ 설계 — dynamic_cast 제거와 정적 타입 분기
- 28임베디드 스마트 포인터 선택 — unique·shared·custom 비교
- 29임베디드 C++ 소유권 모델 — single·shared·borrow 패턴
- 30Intrusive Containers 분석 — 동적 할당 없는 컨테이너 설계
- 31ETL 라이브러리 분석 — Embedded Template Library의 STL 대체
- 32임베디드 Lock-free 기초 — atomic·memory ordering·CAS
- 33Lock-free Container 구현 — SPSC Queue·Ring Buffer
- 34Type-safe Flags 패턴 — Enum Class·Strong Typedef·Tag
- 35임베디드 State Machine 패턴 — Variant·Visitor·Table-driven 비교
- 36Compile-time FSM 구현 — 템플릿으로 상태 전이 검증
- 37Singleton 대안 패턴 — Service Locator·Static Init·Phantom
- 38MMIO Register 추상화 — 타입 안전한 비트 필드 접근
- 39GPIO 추상화 패턴 — Template·Concept으로 보드 독립성
- 40Peripheral 추상화 — UART·SPI·I2C 공통 인터페이스 설계
- 41임베디드 HAL 설계 패턴 — Static·Dynamic·Hybrid 비교
관련 글
GPIO 추상화 패턴 — Template·Concept으로 보드 독립성
GPIO pin = type — 컴파일 타임에 핀 설정 검증, runtime 비용 0.
MMIO Register 추상화 — 타입 안전한 비트 필드 접근
MMIO를 type-safe하게 — volatile, bit field, register wrapper class.
임베디드 State Machine 패턴 — Variant·Visitor·Table-driven 비교
타입 안전한 상태 머신 — enum + switch부터 std::variant, etl::fsm까지.