본문으로 건너뛰기
Previous
Next
C++ bitset | '비트 집합' 가이드 | 핵심 개념과 실전 활용

C++ bitset | '비트 집합' 가이드 | 핵심 개념과 실전 활용

C++ bitset | '비트 집합' 가이드 | 핵심 개념과 실전 활용

이 글의 핵심

비트 연산 기초, bitset vs vector<bool>, 마스킹·순열·조합 패턴, 성능까지 정리한 bitset 가이드입니다.

bitset이란?

std::bitset고정 크기 비트 집합을 표현하는 STL 컨테이너입니다. 비트 단위로 데이터를 효율적으로 저장하고 조작할 수 있으며, 플래그, 상태 관리, 비트 마스크 등에 유용합니다.

C/C++ 예제 코드입니다.

#include <bitset>

// 실행 예제
std::bitset<8> bits;  // 8비트

bits.set(0);    // 0번 비트 1
bits.set(3);    // 3번 비트 1
bits.reset(0);  // 0번 비트 0

왜 필요한가?:

  • 메모리 효율: 1비트 = 1개 bool (일반 배열은 1바이트)
  • 비트 연산: AND, OR, XOR, NOT 등 직관적 연산
  • 타입 안전: 컴파일 타임 크기 검증
  • 편의성: 비트 조작 함수 제공
// ❌ 일반 배열: 8바이트
bool flags[8] = {false};

// ✅ bitset: 1바이트
std::bitset<8> flags;  // 8비트 = 1바이트

비트 인덱싱:

C/C++ 예제 코드입니다.

std::bitset<8> bits{"10101010"};
//                   76543210  (인덱스)

std::cout << bits[0] << '\n';  // 0 (오른쪽)
std::cout << bits[7] << '\n';  // 1 (왼쪽)

// 주의: 문자열과 인덱스 방향이 반대!

기본 사용

#include <bitset>

// 생성
std::bitset<8> b1;              // 00000000
std::bitset<8> b2{0b10101010};  // 10101010
std::bitset<8> b3{"10101010"};  // 10101010

// 접근
bool bit0 = b2[0];
bool bit7 = b2[7];

// 출력
std::cout << b2 << std::endl;  // 10101010

실전 예시

예시 1: 플래그

#include <bitset>

enum Permission {
    Read = 0,
    Write = 1,
    Execute = 2,
    Delete = 3
};

class FilePermissions {
    std::bitset<4> perms;
    
public:
    void grant(Permission p) {
        perms.set(p);
    }
    
    void revoke(Permission p) {
        perms.reset(p);
    }
    
    bool has(Permission p) const {
        return perms[p];
    }
    
    void print() const {
        std::cout << "읽기: " << perms[Read] << std::endl;
        std::cout << "쓰기: " << perms[Write] << std::endl;
        std::cout << "실행: " << perms[Execute] << std::endl;
        std::cout << "삭제: " << perms[Delete] << std::endl;
    }
};

int main() {
    FilePermissions fp;
    fp.grant(Read);
    fp.grant(Write);
    
    if (fp.has(Read)) {
        std::cout << "읽기 가능" << std::endl;
    }
    
    fp.print();
}

예시 2: 비트 연산

#include <bitset>

int main() {
    std::bitset<8> b1{"11110000"};
    std::bitset<8> b2{"10101010"};
    
    // AND
    auto and_result = b1 & b2;
    std::cout << "AND: " << and_result << std::endl;  // 10100000
    
    // OR
    auto or_result = b1 | b2;
    std::cout << "OR: " << or_result << std::endl;  // 11111010
    
    // XOR
    auto xor_result = b1 ^ b2;
    std::cout << "XOR: " << xor_result << std::endl;  // 01011010
    
    // NOT
    auto not_result = ~b1;
    std::cout << "NOT: " << not_result << std::endl;  // 00001111
}

예시 3: 변환

#include <bitset>

std::bitset<8> bits{"10101010"};

// 정수로
unsigned long ul = bits.to_ulong();
std::cout << "정수: " << ul << std::endl;  // 170

// 문자열로
std::string str = bits.to_string();
std::cout << "문자열: " << str << std::endl;  // 10101010

// 정수에서
std::bitset<8> bits2{170};

예시 4: 카운트

#include <bitset>

std::bitset<8> bits{"10101010"};

// 1의 개수
std::cout << "1의 개수: " << bits.count() << std::endl;  // 4

// 크기
std::cout << "크기: " << bits.size() << std::endl;  // 8

// 모두 1?
std::cout << "모두 1: " << bits.all() << std::endl;  // false

// 하나라도 1?
std::cout << "하나라도 1: " << bits.any() << std::endl;  // true

// 모두 0?
std::cout << "모두 0: " << bits.none() << std::endl;  // false

비트 연산

C/C++ 예제 코드입니다.

std::bitset<8> bits;

// 설정
bits.set();      // 모두 1
bits.set(3);     // 3번 비트 1
bits.set(3, 0);  // 3번 비트 0

// 리셋
bits.reset();    // 모두 0
bits.reset(3);   // 3번 비트 0

// 플립
bits.flip();     // 모두 반전
bits.flip(3);    // 3번 비트 반전

// 테스트
bool bit3 = bits.test(3);

비트 연산 기초

정수나 bitset에서 자주 쓰는 연산을 정리하면 다음과 같습니다.

연산의미예시 직관
AND &둘 다 1인 위치만 1마스크로 특정 비트만 추출
OR (비트 OR)하나라도 1이면 1비트 켜기
XOR ^서로 다르면 1토글, 같은지 비교
NOT ~전부 반전전체 마스크 뒤집기
<< / >>왼쪽/오른쪽 시프트(2^k) 배 곱·나눗셈에 해당하는 이동

std::bitset은 위 연산을 비트 집합 전체에 대해 오버로드되어 있어, 루프 없이 한 번에 AND/OR/XOR/NOT을 할 수 있습니다. 정수와 달리 크기가 다른 bitset 간 연산은 컴파일 타임에 크기가 같아야 합니다.

std::bitset<8> a{"11001100"};
std::bitset<8> b{"10101010"};
std::bitset<8> low4 = a & std::bitset<8>{"00001111"};  // 하위 4비트만

bitset vs vector

둘 다 비트 단위 압축을 목표로 하지만 역할이 다릅니다.

항목std::bitset<N>std::vector<bool>
크기컴파일 타임 상수 N (템플릿 인자)런타임resize 가능
저장 위치보통 스택 또는 객체 안에 고정 크기힙에 동적 할당
연산AND·OR·XOR·NOT, 시프트 등 비트 연산 풍부요소별 접근·일부 알고리즘 위주
표준 보장완전한 비트 집합 타입vector의 특수화로 참조자 등 세부가 까다로움

언제 무엇을 쓸까

  • N이 코드에서 고정(예: 플래그 64비트, IPv4 마스크 32비트)이면 bitset 이 타입 안전하고 연산이 명확합니다.
  • 사용자 입력이나 파일 크기에 따라 길이가 바뀌면 vector 또는 비트 길이가 매우 크면 boost::dynamic_bitset 같은 대안을 검토합니다.
  • vector<bool>은 “bool 모음”이지 이론적으로 완전한 vector<T>와 동일하지 않다는 점(예: std::vector<bool>::reference)을 문서에 유의합니다.

비트 마스킹 패턴

특정 비트 켜기/끄기/토글

C/C++ 예제 코드입니다.

unsigned x = 0b1010;
unsigned mask = 1u << 3;   // 3번 비트
x |= mask;                 // 켜기
x &= ~mask;                // 끄기
x ^= mask;                 // 토글

bitset에서는 set(pos), reset(pos), flip(pos)로 동일한 일을 더 읽기 쉽게 할 수 있습니다.

하위 k비트만 유지: x & ((1u << k) - 1)
특정 구간 비트 추출: 시프트와 AND를 조합합니다.

std::bitset<16> v{"1111000011110000"};
auto low8 = (v & std::bitset<16>{"0000000011111111"});  // 개념적으로 하위 8비트

실전: 플래그 관리·순열·조합

플래그 조합

여러 옵션을 한 정수나 bitset에 넣어 비트 OR로 전달하는 패턴은 API 플래그에서 흔합니다.

enum class Opt : unsigned { A = 1u << 0, B = 1u << 1, C = 1u << 2 };

unsigned f = static_cast<unsigned>(Opt::A) | static_cast<unsigned>(Opt::C);
bool hasB = (f & static_cast<unsigned>(Opt::B)) != 0;

bitset으로 옵션 인덱스를 직접 쓰면 디버깅 시 이진 출력이 명확해집니다.

부분집합 열거(비트 마스크)

원소이 (0..n-1)인 집합의 모든 부분집합은 0부터 2^n - 1까지의 마스크로 순회할 수 있습니다.

#include <bitset>
#include <iostream>

int main() {
    const int n = 4;
    // n이 크면 (1u << n) 오버플로에 주의 (unsigned 비트 폭 이하로 사용)
    for (unsigned mask = 0; mask < (1u << n); ++mask) {
        std::bitset<n> bs(mask);
        std::cout << bs << '\n';
    }
}

(n)이 크면 2^n이 폭발하므로 백트래킹이나 다음 부분집합 최적화(예: submask = (submask - 1) & mask)를 상황에 맞게 씁니다.

조합·순열과의 관계

  • 조합: 비트가 1인 위치를 “선택된 원소”로 두는 표현이 자연스럽습니다.
  • 순열: 순서가 중요하므로 비트만으로는 부족하고, next_permutation 등과 함께 쓰이는 경우가 많습니다. 다만 “사용 여부”를 비트로 두는 상태 압축 DP에서는 bitset/uint32_t 마스크가 자주 등장합니다.

성능 최적화

  • 고정 크기·핫 루프: bitset<N>의 단일 연산은 컴파일러가 워드 단위로 최적화하기 좋습니다. N이 작으면 스택에 두고 반복 할당을 피하세요.
  • 카운트: count()로 1의 개수를 한 번에 구할 수 있습니다. 특정 비트를 순회할 때는 인덱스 루프와 test(i)가 표준적인 방법이며, 매우 빠른 최하위 비트 탐색이 필요하면 정수형 마스크와 컴파일러 내장 함수(예: GCC __builtin_ctzll)를 프로젝트 정책에 맞게 검토합니다.
  • I/O: 대량 출력 시 to_string() 반복은 비용이 클 수 있습니다. 디버깅용이 아니면 정수 비트 연산으로 처리하거나 버퍼를 한 번에 쓰는 방식을 검토합니다.
  • vector vs bitset: 동적 크기가 꼭 필요하지 않으면 bitset이 레이아웃이 단순하고 최적화하기 쉬운 경우가 많습니다.

자주 발생하는 문제

문제 1: 크기

C/C++ 예제 코드입니다.

// 컴파일 타임 크기
std::bitset<8> bits;

// ❌ 런타임 크기
// int n = 8;
// std::bitset<n> bits;  // 에러

// ✅ 런타임 크기: vector<bool>
std::vector<bool> bits(n);

문제 2: 인덱스

std::bitset<8> bits{"10101010"};

// 인덱스 0: 오른쪽 비트
std::cout << bits[0] << std::endl;  // 0 (오른쪽)
std::cout << bits[7] << std::endl;  // 1 (왼쪽)

문제 3: 변환

C/C++ 예제 코드입니다.

std::bitset<64> bits{0xFFFFFFFFFFFFFFFF};

// ❌ 오버플로우
// unsigned long ul = bits.to_ulong();  // 32비트 시스템에서 에러

// ✅ unsigned long long
unsigned long long ull = bits.to_ullong();

문제 4: 문자열

// 문자열 생성
std::bitset<8> bits{"10101010"};

// ❌ 잘못된 문자
// std::bitset<8> bits2{"102"};  // 예외

// ✅ 유효성 검사
try {
    std::bitset<8> bits3{"10201010"};
} catch (const std::invalid_argument&) {
    std::cout << "유효하지 않은 문자" << std::endl;
}

실무 패턴

패턴 1: 상태 머신

enum class State {
    Idle = 0,
    Running = 1,
    Paused = 2,
    Error = 3
};

class StateMachine {
    std::bitset<4> activeStates_;
    
public:
    void enterState(State s) {
        activeStates_.set(static_cast<int>(s));
    }
    
    void exitState(State s) {
        activeStates_.reset(static_cast<int>(s));
    }
    
    bool isInState(State s) const {
        return activeStates_[static_cast<int>(s)];
    }
    
    bool isIdle() const {
        return activeStates_.none();  // 모든 비트 0
    }
};

// 사용
StateMachine sm;
sm.enterState(State::Running);
if (sm.isInState(State::Running)) {
    std::cout << "실행 중\n";
}

패턴 2: 집합 연산

class CharSet {
    std::bitset<256> chars_;  // ASCII 문자 집합
    
public:
    void add(char c) {
        chars_.set(static_cast<unsigned char>(c));
    }
    
    bool contains(char c) const {
        return chars_[static_cast<unsigned char>(c)];
    }
    
    CharSet operator&(const CharSet& other) const {
        CharSet result;
        result.chars_ = chars_ & other.chars_;
        return result;
    }
    
    CharSet operator|(const CharSet& other) const {
        CharSet result;
        result.chars_ = chars_ | other.chars_;
        return result;
    }
    
    size_t size() const {
        return chars_.count();
    }
};

// 사용
CharSet vowels;
vowels.add('a'); vowels.add('e'); vowels.add('i');

CharSet consonants;
consonants.add('b'); consonants.add('c');

auto intersection = vowels & consonants;  // 교집합
auto union_set = vowels | consonants;     // 합집합

패턴 3: 비트 필드 압축

class CompactData {
    std::bitset<32> flags_;
    
public:
    // 0-7: 타입 (8비트)
    void setType(uint8_t type) {
        for (int i = 0; i < 8; ++i) {
            flags_[i] = (type >> i) & 1;
        }
    }
    
    uint8_t getType() const {
        uint8_t type = 0;
        for (int i = 0; i < 8; ++i) {
            if (flags_[i]) {
                type |= (1 << i);
            }
        }
        return type;
    }
    
    // 8-15: 상태 플래그
    void setFlag(int index, bool value) {
        flags_[8 + index] = value;
    }
    
    bool getFlag(int index) const {
        return flags_[8 + index];
    }
};

FAQ

Q1: bitset은 무엇인가요?

A: 고정 크기 비트 집합을 표현하는 STL 컨테이너입니다. 비트 단위로 데이터를 효율적으로 저장하고 조작할 수 있습니다.

Q2: bitset의 크기는 어떻게 결정되나요?

A: 컴파일 타임에 템플릿 인자로 고정됩니다. 런타임에 크기를 변경할 수 없습니다.

std::bitset<8> bits;  // 8비트 고정

Q3: 어떤 비트 연산을 지원하나요?

A: AND(&), OR(|), XOR(^), NOT(~), 왼쪽 시프트(<<), 오른쪽 시프트(>>)를 지원합니다.

std::bitset<8> b1{"11110000"};
std::bitset<8> b2{"10101010"};

auto result = b1 & b2;  // AND

Q4: 정수나 문자열로 변환할 수 있나요?

A: 가능합니다. to_ulong(), to_ullong(), to_string()을 사용합니다.

std::bitset<8> bits{"10101010"};
unsigned long ul = bits.to_ulong();
std::string str = bits.to_string();

Q5: vector과 차이는?

A:

  • bitset: 고정 크기, 컴파일 타임 결정
  • vector: 동적 크기, 런타임 변경 가능
std::bitset<8> bits;  // 고정
std::vector<bool> vec(8);  // 동적
vec.resize(16);  // OK

Q6: bitset의 메모리 크기는?

A: 비트 수를 8로 나눈 값(바이트 단위)입니다. 예를 들어, bitset<8>은 1바이트, bitset<32>는 4바이트입니다.

Q7: 비트 인덱스 방향은?

A: 인덱스 0은 오른쪽 비트입니다.

std::bitset<8> bits{"10101010"};
//                   76543210  (인덱스)
bits[0];  // 0 (오른쪽)
bits[7];  // 1 (왼쪽)

Q8: bitset 학습 리소스는?

A:

관련 글: Bit Manipulation, Bit Fields.

한 줄 요약: std::bitset은 고정 크기 비트 집합으로, 메모리 효율적이고 비트 연산을 직관적으로 수행할 수 있습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

관련 글

심화 부록: 구현·운영 관점

이 부록은 앞선 본문에서 다룬 주제(「C++ bitset | ‘비트 집합’ 가이드」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.

내부 동작과 핵심 메커니즘

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]
sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(런타임·게이트웨이·프로세스)
  participant D as 의존성(API·DB·큐·파일)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)
  • 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
  • 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.

프로덕션 운영 패턴

영역운영 관점 질문
관측성요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가
안전성입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가
신뢰성재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가
성능캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가
용량피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가

스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.

확장 예시: 엔드투엔드 미니 시나리오

앞선 본문 주제(「C++ bitset | ‘비트 집합’ 가이드」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
  5. 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)
  authorize(validated, ctx)
  result = domainCore(validated)
  persistOrEmit(result, idempotentKey)
  recordMetrics(ctx, latency, outcome)
  return result

문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스, 타임아웃, 외부 의존성, DNS최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검
성능 저하N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납상한·TTL·힙/FD 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정 불일치프로필·시크릿·기본값, 리전스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

배포 전에는 git addgit commitgit pushnpm run deploy 순서를 권장합니다.


이 글에서 다루는 키워드 (관련 검색어)

C++, bitset, bit, flags, STL 등으로 검색하시면 이 글이 도움이 됩니다.