C++ bitset | "비트 집합" 가이드

C++ bitset | "비트 집합" 가이드

이 글의 핵심

고정 크기 비트 집합 bitset의 연산, vector<bool>과의 선택, 마스킹·부분집합 실전 패턴을 다룹니다.

bitset이란?

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

#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바이트

비트 인덱싱:

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

비트 연산

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

비트 마스킹 패턴

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

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: 크기

// 컴파일 타임 크기
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: 변환

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++ Algorithm Set | “집합 알고리즘” 가이드
  • C++ Custom Allocator | “커스텀 할당자” 가이드
  • C++ 함수 객체 | “Functor” 완벽 가이드

관련 글

  • C++ Algorithm Copy |
  • C++ Algorithm Count |
  • C++ Algorithm Generate |
  • C++ 알고리즘 |
  • C++ Algorithm Heap |