비트 필드(Bit-field)
비트 필드(Bit-field)
클래스 안에서 어떤 데이터 멤버가 차지하는 크기를 비트 단위로 명시하고 싶을 때가 있어요. 그럴 때 쓰는 게 바로 **비트 필드(bit-field)**예요. 비트 필드는 명시된 크기(비트 단위)를 가진 클래스 데이터 멤버를 선언하는 방법이고, 서로 인접한 비트 필드 멤버들은 묶여서(pack) 개별 바이트를 공유하거나 걸쳐서( straddle) 차지할 수도 있어요(이 동작은 구현에 따라 달라져요).
비트 필드 선언은 다음과 같은 선언자를 사용하는 클래스 데이터 멤버 선언이에요.
| 문법 | ||
|---|---|---|
식별자(optional) 속성(optional) : 크기 |
(1) | |
식별자(optional) 속성(optional) : 크기 brace-or-equal-initializer |
(2) | (C++20부터) |
| 자리 | 의미 |
|---|---|
| 속성(attr) | (C++11부터) 임의 개수의 속성들의 시퀀스 |
| 식별자(identifier) | 선언되는 비트 필드의 이름. 이름은 생략할 수 있어요. 이름 없는 비트 필드는 지정된 개수의 패딩 비트를 도입해요. |
| 크기(size) | 값이 0 이상인 적분 상수 표현식. 0보다 크면 비트 필드가 차지할 비트 수예요. 값 0은 이름 없는 비트 필드에만 허용되고 특별한 의미를 가져요. |
| brace-or-equal-initializer | 이 비트 필드에 사용할 기본 멤버 초기화(default member initializer) |
출처: cppreference
본문
비트 필드 타입 제약
비트 필드의 타입은 적분 타입(bool 포함)이거나 (cv-한정될 수 있는) 열거 타입뿐이에요. 이름 없는 비트 필드는 cv-한정 타입으로 선언할 수 없어요. 비트 필드는 정적 데이터 멤버가 될 수도 없어요.
비트 필드에는 prvalue가 없어요. lvalue-to-rvalue 변환은 항상 비트 필드의 기반 타입(underlying type) 객체를 만들어 내요.
담을 수 있는 값의 범위
비트 필드의 비트 수가 그 필드가 담을 수 있는 값의 범위를 정해요.
#include <iostream>
struct S
{
// 3비트 부호 없는 필드, 허용 값은 0...7
unsigned int b : 3;
};
int main()
{
S s = {6};
++s.b; // 비트 필드에 값 7을 저장
std::cout << s.b << '\n';
++s.b; // 값 8은 이 비트 필드에 들어가지 못해요
std::cout << s.b << '\n'; // 공식적으로는 구현 정의, 보통 0
}
가능한 출력:
7
0
인접한 비트 필드의 배치
여러 인접한 비트 필드는 보통 서로 묶여서 배치돼요(물론 이 동작은 구현 정의예요).
#include <bit>
#include <cstdint>
#include <iostream>
struct S
{
// 보통 2바이트를 차지해요:
unsigned char b1 : 3; // (1번째 바이트의) 처음 3비트가 b1
unsigned char : 2; // 1번째 바이트의 다음 2비트는 사용하지 않음으로 막아 둠
unsigned char b2 : 6; // b2에 6비트 - 1번째 바이트에 안 들어가므로 2번째 바이트에서 시작
unsigned char b3 : 2; // b3에 2비트 - 2번째 바이트의 다음(그리고 마지막) 비트
};
int main()
{
std::cout << sizeof(S) << '\n'; // 보통 2를 출력
S s;
// 각 필드에 구별되는 값 설정
s.b1 = 0b111;
s.b2 = 0b101111;
s.b3 = 0b11;
// S 안 필드들의 배치를 보여 주기
auto i = std::bit_cast<std::uint16_t>(s);
// 보통 1110000011110111 을 출력
// 구조를 보면: └┬┘├┘└┬┘└─┬──┘└┤
// b1 u a b2 b3
// "u"는 구조체에서 지정한 사용하지 않는 :2 를,
// "a"는 다음 필드를 바이트 정렬하기 위해 컴파일러가 추가한 패딩을 가리켜요.
// b2의 타입이 unsigned char로 선언됐기 때문에 바이트 정렬이 일어나요.
// b2를 uint16_t로 선언했다면 "a"가 없어져서 b2가 "u"에 바로 붙었을 거예요.
for (auto b = i; b; b >>= 1) // LSB부터 출력
std::cout << (b & 1);
std::cout << '\n';
}
가능한 출력:
2
1110000011110111
크기 0의 이름 없는 비트 필드
특별한 경우로, 크기가 0인 이름 없는 비트 필드는 패딩을 강제로 끊는 데 쓸 수 있어요. 다음 비트 필드가 그 할당 단위(allocation unit)의 처음부터 시작하도록 지정하는 거예요.
#include <iostream>
struct S
{
// 보통 2바이트를 차지해요:
// 3비트: b1의 값
// 5비트: 사용하지 않음
// 2비트: b2의 값
// 6비트: 사용하지 않음
unsigned char b1 : 3;
unsigned char :0; // 새 바이트 시작
unsigned char b2 : 2;
};
int main()
{
std::cout << sizeof(S) << '\n'; // 보통 2 출력
// 패딩 끊김이 없다면 보통 1이었을 거예요
}
가능한 출력:
2
크기와 타입의 관계, 참조 한계
비트 필드의 지정 크기가 그 타입의 크기보다 크다면 값은 타입에 의해 제한돼요. std::uint8_t b : 1000;처럼 선언해도 여전히 [0, 255] 범위의 값만 담을 수 있고, 나머지 비트는 패딩 비트가 돼요.
비트 필드는 반드시 바이트의 처음부터 시작하지 않기 때문에 비트 필드의 주소를 얻을 수 없어요. 비트 필드에 대한 포인터나 비const 참조도 만들 수 없어요. 비트 필드로부터 const 참조를 초기화할 때는 임시 객체(타입은 비트 필드의 타입)가 만들어지고, 비트 필드 값으로 복사 초기화된 그 임시 객체에 참조가 바인딩돼요.
비트 필드에는 기본 멤버 초기화가 없어요. int b : 1 = 0;이나 int b : 1 { 0 }은 잘못된 형태(ill-formed)예요. (C++20 이전)
비트 필드의 크기와 기본 멤버 초기화 사이에 모호함이 생길 때는, 유효한 크기를 이루는 가장 긴 토큰 시퀀스가 선택돼요. (C++20부터)
int a;
const int b = 0;
struct S
{
// 간단한 경우
int x1 : 8 = 42; // OK; "= 42"가 brace-or-equal-initializer
int x2 : 8 {42}; // OK; "{42}"가 brace-or-equal-initializer
// 모호한 경우
int y1 : true ? 8 : a = 42; // OK; brace-or-equal-initializer 없음
int y2 : true ? 8 : b = 42; // error: const int에 대입 불가
int y3 : (true ? 8 : b) = 42; // OK; "= 42"가 brace-or-equal-initializer
int z : 1 || new int {0}; // OK; brace-or-equal-initializer 없음
};
주의할 점(Notes)
비트 필드의 다음 속성들은 **구현 정의(implementation-defined)**예요.
- 범위를 벗어난 값으로 부호 있는 비트 필드에 대입·초기화하거나, 범위를 넘어 증가시켰을 때 결과가 되는 값.
- 클래스 객체 안 비트 필드의 실제 배치 세부 사항 전부.
- 비트 필드의 정렬(alignment).
C 언어에서는 비트 필드의 폭이 기반 타입의 폭을 넘을 수 없고, 명시적으로 signed나 unsigned로 선언되지 않은 int 비트 필드가 부호 있는지 여부도 구현 정의예요. 예를 들어 int b : 3;은 C에서는 [0, 7] 또는 [-4, 3] 범위를 가질 수 있는데, C++에서는 후자(부호 있는)만 허용돼요.
결함 보고(Defect reports)
비트 필드와 관련해 이전에 발표된 C++ 표준들에 소급 적용된 동작 변경 결함 보고들이 있어요.
| DR | 적용 대상 | 발표 당시 동작 | 올바른 동작 |
|---|---|---|---|
| CWG 324 | C++98 | 비트 필드에 대한 대입의 반환 값이 비트 필드인지가 명시되지 않았음 | lvalue를 반환할 수 있는 연산자들에 비트 필드 명세 추가 |
| CWG 739 | C++98 | signed도 unsigned도 아닌 비트 필드의 부호 여부가 구현 정의였음 |
기반 타입과 일관되게 함 |
| CWG 2229 | C++98 | 이름 없는 비트 필드를 cv-한정 타입으로 선언할 수 있었음 | 금지됨 |
| CWG 2511 | C++98 | 비트 필드 타입에 cv-한정이 허용되지 않았음 | 비트 필드는 cv-한정 열거 타입을 가질 수 있음 |
더 알아보기
bitset: 고정 길이 비트 배열을 구현한 클래스 템플릿.vector<bool>: 공간 효율적인 동적 비트셋 (클래스 템플릿 특수화).- 비트 조작(Bit manipulation, C++20): 개별 비트와 비트 시퀀스에 접근·조작·처리하는 유틸리티.