비트 플래그 (bit flag)
메모리의 최소 크기 단위는 1바이트이므로 변수의 크기는 적어도 1바이트 이상이다. 8비트(1바이트)는 비트가 8개이므로 8가지 상태를 저장할 수 있다. 이는 1바이트를 사용해서 1비트만 사용하고 7비트를 낭비함으로써 1가지 상태만 저장하는 bool 자료형보다 훨씬 효율적이다.
플래그(flag)는 깃발에서 유래한 용어다. 보통 깃발을 위로 올리면 on, 아래로 내리면 off를 뜻한다. 이걸 정수의 비트에 활용하는 건데 비트가 1이면 on, 0이면 off를 나타낸다.
여기서 바이트의 개별 비트를 비트 플래그(bit flag)라고 한다.
플래그를 설명할 때는 일반적으로 오른쪽에서 왼쪽으로 센다. 예를 들어, 아래는 두 번째와 여덟 번째 비트가 켜진 상태다.
0100 0001 // 두 번째 비트와 여덟 번째 비트가 켜진 상태(on)
C++ 14에서 비트 플래그 정의 (Defining bit flags in C++14)
비트 플래그를 사용하려면 바이트 내에서 개별 비트를 식별할 수 있는 방법을 사용해서 비트를 조작해야 한다. 해당 비트를 나타내는 기호 상수를 정의하는 방식으로 조작할 수 있다.
C++ 14는 바이너리 리터럴을 제공한다:
// Define 8 separate bit flags (these can represent whatever you want)
const unsigned char option0 = 0b0000'0001; // represents bit 0
const unsigned char option1 = 0b0000'0010; // represents bit 1
const unsigned char option2 = 0b0000'0100; // represents bit 2
const unsigned char option3 = 0b0000'1000; // represents bit 3
const unsigned char option4 = 0b0001'0000; // represents bit 4
const unsigned char option5 = 0b0010'0000; // represents bit 5
const unsigned char option6 = 0b0100'0000; // represents bit 6
const unsigned char option7 = 0b1000'0000; // represents bit 7
이제 각 비트 위치를 나타내는 일련의 기호 상수가 있다. 이 기호 상수를 비트를 조작하는 데 사용할 수 있다.
C++ 11 또는 이전 버전에서 비트 플래그 정의 (Defining bit flags in C++11 or earlier)
C++ 11은 바이너리 리터럴을 제공하지 않으므로 기호 상수를 설정하는 데 다른 방법을 사용해야 한다.
16진수를 사용하는 게 일반적인 방법이다:
// Define 8 separate bit flags (these can represent whatever you want)
const unsigned char option0 = 0x1; // hex for 0000 0001
const unsigned char option1 = 0x2; // hex for 0000 0010
const unsigned char option2 = 0x4; // hex for 0000 0100
const unsigned char option3 = 0x8; // hex for 0000 1000
const unsigned char option4 = 0x10; // hex for 0001 0000
const unsigned char option5 = 0x20; // hex for 0010 0000
const unsigned char option6 = 0x40; // hex for 0100 0000
const unsigned char option7 = 0x80; // hex for 1000 0000
읽기는 조금 어렵지만, 왼쪽 시프트 연산자(<<)를 사용한 더 쉬운 방법도 있다:
// Define 8 separate bit flags (these can represent whatever you want)
const unsigned char option0 = 1 << 0; // 0000 0001
const unsigned char option1 = 1 << 1; // 0000 0010
const unsigned char option2 = 1 << 2; // 0000 0100
const unsigned char option3 = 1 << 3; // 0000 1000
const unsigned char option4 = 1 << 4; // 0001 0000
const unsigned char option5 = 1 << 5; // 0010 0000
const unsigned char option6 = 1 << 6; // 0100 0000
const unsigned char option7 = 1 << 7; // 1000 0000
플래그를 사용한 비트 조작 (Using bit flags to manipulate bits)
다음으로 필요한 것은 조작하고자 하는 변수다. 사용하는 옵션(option) 수에 따라 적절한 크기(8비트, 16비트, 32비트 등)의 부호 없는(unsigned) 정수 자료형을 사용한다.
// 위에서 정의한 8가지 옵션을 위해 8비트를 사용한다.
unsigned char myflags = 0; // all bits turned off to start
비트 켜기 (Turning individual bits on)
비트 OR 연산자(|)를 사용해 비트를 켤 수 있다.
myflags |= option4; // turn option 4 on
myflags |= option4는 myflags = (myflags | option4)와 같다:
myflags = 0000 0000 (we initialized this to 0)
option4 = 0001 0000
-------------------
result = 0001 0000
비트 끄기 (Turning individual bits off)
비트 AND 연산자(&)와 비트 NOT 연산자(~)를 이용해서 비트를 끌 수 있다.
myflags &= ~option4; // turn option 4 off
myflags가 처음에 0001 1100이라고 가정하자. (option3, 4 및 5가 켜진 상태)
myflags &= ~option4는 myflags = (myflags & ~option4)와 같다:
myflags = 0001 1100
~option4 = 1110 1111
--------------------
result = 0000 1100
그래서 0000 1100이 다시 할당된다. 즉, 네 번째 비트를 끈 것이다.
여러 비트를 동시에 끌 수도 있다.
myflags &= ~(option4 | option5); // turn options 4 and 5 off at the same time
비트 뒤집기 (Flipping individual bits)
비트 XOR 연산자(^)를 이용해서 비트를 토글(toggle)할 수 있다.
myflags ^= option4; // flip option4 from on to off, or vice versa
myflags ^= (option4 | option5); // flip options 4 and 5 at the same time
비트가 켜져 있는지 꺼져 있는지 확인하기 (Determining if a bit is on or off)
비트 AND 연산자(&)를 이용해서 비트의 상태를 알 수 있다.
if (myflags & option4)
std::cout << "myflags has option 4 set";
if !(myflags & option5)
std::cout << "myflags does not have option 5 set";
비트 플래그가 유용한 이유 (Why are bit flags useful?)
1. 옵션(or 상태)가 많이 필요할 때:
myflag 같은 bool 자료형 변수가 하나가 아니라 myflag1, myflag2 ... 처럼 n개의 옵션이 필요하다고 가정해보자. 8개의 옵션(or 상태)을 정의하려면 각각 true, false가 필요하므로 16개의 bool 자료형 변수를 정의해야 하고, 16바이트 메모리를 사용한다 (옵션이 많을수록 더 많은 메모리를 사용한다) 그러나 비트 플래그를 사용하면 8개의 옵션을 사용할 때, 1바이트(8비트)로 충분하다. 즉, 메모리를 절약할 수 있다.
2. 옵션(or 상태)을 조합할 때:
32가지 옵션을 이용해 사용할 수 있는 함수가 있다고 가정해보자. 이 함수를 호출하려면 32개의 매개 변수(parameter)를 사용할 것이다.
void someFunction(bool option1, bool option2, bool option3, bool option4, bool option5, bool option6, bool option7, bool option8, bool option9, bool option10, bool option11, bool option12, bool option13, bool option14, bool option15, bool option16, bool option17, bool option18, bool option19, bool option20, bool option21, bool option22, bool option23, bool option24, bool option25, bool option26, bool option27, bool option28, bool option29, bool option30, bool option31, bool option32);
매개 변수 이름을 보고 옵션의 기능을 유추할 수 없을뿐더러 매개 변수의 목록이 너무 많다.
option10과 option32가 true로 설정된 함수를 호출하려면 다음과 같이 해야한다.
someFunction(false, false, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, true);
읽기 매우 어렵고 어떤 매개 변수가 어떤 옵션에 해당하는지 기억해야 한다. 또한, 호출할 때마다 32개의 bool 값을 복사해야 하므로 성능이 안 좋을 수 있다.
대신, 다음과 같이 비트 플래그를 사용하여 함수를 정의하면:
void someFunction(unsigned int options);
비트 플래그를 이용해서 원하는 옵션만 전달할 수 있다.
someFunction(option10 | option32);
이 같은 방법은 읽기 훨씬 더 쉬울 뿐만 아니라, 2개의 연산(비트 OR 연산 1개와 파라미터 복사본 1개)만 포함하기 때문에 더 성능이 좋다.
또한, 나중에 옵션을 추가해야 할 때에는 비트 플래그를 정의하기만 하면 된다.
Bit flags in real life
3D 그래픽 라이브러리인 'OpenGL'에서 몇몇 함수는 비트 플래그를 매개 변수로 사용한다.
glClearg (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // clear the color and the depth buffer
GL_COLOR_BUFFER_BIT와 GL_DEPTH_BUFFER_BIT는 다음과 같이 정의되어 있다:
#define GL_DEPTH_BUFFER_BIT 0x00000100
#define GL_STENCIL_BUFFER_BIT 0x00000400
#define GL_COLOR_BUFFER_BIT 0x00004000
아래는 게임에 대한 추상적인 코드다:
#include <iostream>
int main()
{
// Define a bunch of physical/emotional states
const unsigned char isHungry = 1 << 0; // 0000 0001
const unsigned char isSad = 1 << 1; // 0000 0010
const unsigned char isMad = 1 << 2; // 0000 0100
const unsigned char isHappy = 1 << 3; // 0000 1000
const unsigned char isLaughing = 1 << 4; // 0001 0000
const unsigned char isAsleep = 1 << 5; // 0010 0000
const unsigned char isDead = 1 << 6; // 0100 0000
const unsigned char isCrying = 1 << 7; // 1000 0000
unsigned char me = 0; // all flags/options turned off to start
me |= isHappy | isLaughing; // I am happy and laughing
me &= ~isLaughing; // I am no longer laughing
// Query a few states (we'll use static_cast<bool> to interpret the results as a boolean value rather than an integer)
std::cout << "I am happy? " << static_cast<bool>(me & isHappy) << '\n';
std::cout << "I am laughing? " << static_cast<bool>(me & isLaughing) << '\n';
return 0;
}
std::bitset
C++ 표준 라이브러리는 비트 플래그 조작을 돕는 std::biset을 제공한다.
std::bitset을 사용하려면 <bitset> 헤더 파일을 #include 한 다음 필요한 비트 수를 나타내는 std::bitset 변수를 정의해야 한다.
#include <bitset>
std::bitset<8> bits; // we need 8 bits
필요한 경우 초기값 집합을 사용하여 비트 집합을 초기화할 수 있다.
#include <bitset>
std::bitset<8> bits(option1 | option2) ; // start with option 1 and 2 turned on
std::bitset<8> morebits(0x3) ; // start with bit pattern 0000 0011
초기값은 이진수로 해석되므로 값 3은 0000 0011이 된다.
std:bitset은 4가지 주요 함수를 제공한다.
- test(): 비트의 상태를 알려준다.
- set(): 비트를 켠다.
- reset(): 비트를 끈다.
- flip(): 비트를 뒤집는다.
#include <bitset>
#include <iostream>
// Note that with std::bitset, our options correspond to bit indices, not bit patterns
const int option0 = 0;
const int option1 = 1;
const int option2 = 2;
const int option3 = 3;
const int option4 = 4;
const int option5 = 5;
const int option6 = 6;
const int option7 = 7;
int main()
{
std::bitset<8> bits(0x2); // we need 8 bits, start with bit pattern 0000 0010
bits.set(option4); // set bit 4 to 1 (now we have 0001 0010)
bits.flip(option5); // flip bit 5 (now we have 0011 0010)
bits.reset(option5); // set bit 5 back to 0 (now we have 0001 0010)
std::cout << "Bit 4 has value: " << bits.test(option4) << '\n';
std::cout << "Bit 5 has value: " << bits.test(option5) << '\n';
std::cout << "All the bits: " << bits << '\n';
return 0;
}
This prints:
Bit 4 has value: 1
Bit 5 has value: 0
All the bits: 00010010
std::bitset을 std::cout을 이용해 출력하면 모든 비트 값이 출력된다.
std:bitset은 표준 비트 단위 연산자(|, &, ^)도 지원하므로 원하는 경우 사용할 수 있다. bitset이 더 편리하고 오류 발생률이 낮으므로 사용하는 것을 추천한다.
출처: https://boycoding.tistory.com/164 [소년코딩]