본문으로 건너뛰기
Embedded C++ for Real Systems · 33/41

Type-safe Flags 패턴 — Enum Class·Strong Typedef·Tag

· Hawk · 3분 읽기

#한 줄 요약

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; // OK
int 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,
};
// 비트 OR
constexpr Flag operator|(Flag a, Flag b) noexcept {
return static_cast<Flag>(
static_cast<uint32_t>(a) | static_cast<uint32_t>(b));
}
// 비트 AND
constexpr Flag operator&(Flag a, Flag b) noexcept {
return static_cast<Flag>(
static_cast<uint32_t>(a) & static_cast<uint32_t>(b));
}
// 비트 NOT
constexpr Flag operator~(Flag a) noexcept {
return static_cast<Flag>(~static_cast<uint32_t>(a));
}
// 비트 XOR
constexpr 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; // toggle

C++ 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 }; // 옛 enum
int 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 int
int flags = (1 << 0) | (1 << 1);
// V2 — enum class with operators
auto 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 4-06: State Machine — 상태와 전이를 type-safe하게. enum + switch부터 std::variant까지.

Embedded C++ for Real Systems · 34 of 41

  1. 1Embedded C++ for Real Systems — 임베디드 모던 C++ 시리즈 소개
  2. 2임베디드 C++ vs C — 런타임·코드 크기·ABI 관점 비교
  3. 3임베디드 C++ 컴파일러 플래그 분석 — -fno-rtti·-fno-exceptions·-Os
  4. 4임베디드 C++ 런타임 요구사항 — libstdc++·newlib·crt0 분석
  5. 5C++ 코드 크기 분석 — 가상 함수·템플릿·예외 비용 추적
  6. 6C++ ABI 호환성 — Itanium ABI·name mangling·vtable 레이아웃
  7. 7C++ 스타트업 코드 분석 — .init_array·전역 생성자 호출 순서
  8. 8임베디드 C++ 링커 스크립트 — vtable·정적 객체 배치 추적
  9. 9임베디드 C++ 표준 선택 가이드 — C++11/14/17/20/23 트레이드오프
  10. 10임베디드 RAII 기초 — 리소스 안전성과 결정적 소멸 보장
  11. 11임베디드 RAII 실전 패턴 — Lock·Pin·DMA·Power 관리
  12. 12constexpr 기초와 임베디드 적용 — 컴파일 타임 계산 활용
  13. 13constexpr 고급 활용 — 룩업 테이블·CRC·해시 컴파일 타임 생성
  14. 14consteval과 constinit 분석 — C++20 컴파일 타임 강제 메커니즘
  15. 15임베디드 Templates 기초 — 타입 안전과 코드 재사용 분석
  16. 16Template 비용 분석 — 코드 폭증·인스턴스화·디버그 정보 측정
  17. 17CRTP 패턴 분석 — vtable 없는 정적 다형성
  18. 18Type Traits 임베디드 활용 — SFINAE·is_pod·컴파일 타임 검사
  19. 19C++20 Concepts 활용 — 템플릿 제약과 가독성 개선
  20. 20동적 할당 없는 임베디드 C++ — placement new·정적 객체·풀
  21. 21Custom Allocator 기초 — std::allocator 인터페이스 분석
  22. 22Pool Allocator 구현 — Fixed-Size Block과 O(1) 보장
  23. 23std::pmr 임베디드 활용 — Polymorphic Memory Resource 분석
  24. 24No-Exception C++ 설계 — 코드 크기·결정성 트레이드오프
  25. 25임베디드 에러 처리 패턴 — Result·errno·optional 비교
  26. 26std::expected 분석 — C++23 결과 타입과 에러 전파
  27. 27No-RTTI C++ 설계 — dynamic_cast 제거와 정적 타입 분기
  28. 28임베디드 스마트 포인터 선택 — unique·shared·custom 비교
  29. 29임베디드 C++ 소유권 모델 — single·shared·borrow 패턴
  30. 30Intrusive Containers 분석 — 동적 할당 없는 컨테이너 설계
  31. 31ETL 라이브러리 분석 — Embedded Template Library의 STL 대체
  32. 32임베디드 Lock-free 기초 — atomic·memory ordering·CAS
  33. 33Lock-free Container 구현 — SPSC Queue·Ring Buffer
  34. 34Type-safe Flags 패턴 — Enum Class·Strong Typedef·Tag
  35. 35임베디드 State Machine 패턴 — Variant·Visitor·Table-driven 비교
  36. 36Compile-time FSM 구현 — 템플릿으로 상태 전이 검증
  37. 37Singleton 대안 패턴 — Service Locator·Static Init·Phantom
  38. 38MMIO Register 추상화 — 타입 안전한 비트 필드 접근
  39. 39GPIO 추상화 패턴 — Template·Concept으로 보드 독립성
  40. 40Peripheral 추상화 — UART·SPI·I2C 공통 인터페이스 설계
  41. 41임베디드 HAL 설계 패턴 — Static·Dynamic·Hybrid 비교