비트 연산자
정수의 비트를 AND·OR·XOR·NOT과 시프트로 조작할 때 적용되는 승격, 너비, 마스크 계약을 익힙니다.
비트 연산자는 정수의 이진 표현에서 같은 위치의 비트를 결합하거나 자리를 옮깁니다. 논리 연산자 &&, ||, !처럼 피연산자를 참·거짓 하나로 줄이지 않으며, 기본 제공 &, |, ^에는 단락 평가도 없습니다.
비트 패턴 자체가 데이터 계약인 플래그, 프로토콜 필드, 장치 레지스터 같은 곳에서는 유용합니다. 일반적인 상태 모델이라면 이름 있는 필드나 자료구조가 더 읽기 쉬울 수 있습니다.
피연산자와 변환 계약
기본 제공 비트 연산자의 피연산자는 정수형 또는 범위 없는 열거형이어야 합니다. float와 double에는 적용할 수 없습니다.
- 이항
&,|,^는 일반 산술 변환을 수행한 공통 타입에서 각 비트를 결합합니다. - 단항
~와 시프트의 피연산자에는 정수 승격이 적용됩니다. unsigned char처럼int보다 작은 타입도 연산 전에 대개int로 승격됩니다.- 범위 있는
enum class는 기본 제공 정수 비트 연산에 암시적으로 참여하지 않습니다. 명시적인 표현이나 연산자 오버로드로 계약을 정의해야 합니다.
예를 들어 4비트만 적어 보면 5는 0101, 3은 0011입니다.
| 연산자 | 자리별 규칙 | 0101과 0011 예시 |
|---|---|---|
& | 둘 다 1인 자리만 1 | 0001 |
| | 하나 이상 1이면 1 | 0111 |
^ | 서로 다른 자리만 1 | 0110 |
~ | 승격된 피연산자의 모든 자리를 반전 | ~0101은 실제 승격 타입의 전체 너비에서 계산 |
작은 정수 타입의 ~ 결과를 원래 너비로 자동 해석하면 안 됩니다.
#include <cstdint>
#include <type_traits>
std::uint8_t small = 5;
auto inverted = ~small;
static_assert(std::is_same_v<decltype(inverted), int>);
// uint8_t가 제공되는 구현에서는 정확히 8비트입니다.
auto low_eight_bits = static_cast<std::uint8_t>(inverted); // 250고정 너비 비트 패턴을 다룰 때는 std::uint32_t처럼 의도가 드러나는 부호 없는 타입을 우선 고려하세요.
#include <cstdint>
std::uint32_t a = 0b0101;
std::uint32_t b = 0b0011;
auto common = a & b; // 0001
auto combined = a | b; // 0111
auto different = a ^ b; // 0110
std::uint32_t inverted = static_cast<std::uint32_t>(~a); // 승격 뒤 32비트로 복귀시프트 연산자
E1 << n과 E1 >> n은 곱셈·나눗셈의 빠른 철자가 아니라, 승격된 왼쪽 피연산자의 고정 너비에서 비트 자리를 옮기는 연산입니다.
연산 전에 다음 계약을 확인합니다.
- 두 피연산자는 정수형 또는 범위 없는 열거형이고 정수 승격을 거칩니다.
- 결과 타입은 승격된
E1의 타입입니다. - 시프트 횟수
n은 음수가 아니며 결과 타입의 너비N보다 작아야 합니다. 그렇지 않으면 동작이 정의되지 않습니다. - 현재 표준 규칙에서 왼쪽 시프트 값은
E1 × 2ⁿ과2ᴺ을 법으로 합동인 결과이며 빈자리는0으로 채웁니다. - 오른쪽 시프트 값은
E1 / 2ⁿ을 음의 무한대 방향으로 반올림합니다. 부호 있는 정수의 오른쪽 시프트는 부호 확장을 수행합니다.
따라서 현재 표준에서 -5 >> 1은 -3입니다. 오래된 C++ 버전을 설명하는 자료에는 음수의 오른쪽 시프트를 구현 정의라고 적은 경우가 있으므로, 대상 언어 버전을 확인해야 합니다.
#include <cstdint>
#include <limits>
std::uint32_t value = 10;
constexpr unsigned count = 2;
static_assert(count < std::numeric_limits<std::uint32_t>::digits);
std::uint32_t left = value << count; // 40
std::uint32_t right = value >> 1; // 5작은 타입은 시프트 전에 승격될 수 있으므로, 결과를 다시 좁은 타입에 저장할 때 별도의 변환이 일어난다는 점도 확인해야 합니다.
BUILT-IN SHIFT CONTRACT
연산 기호보다 먼저 정수 승격 뒤의 왼쪽 타입과 시프트 횟수 범위를 확인합니다.
두 연산의 공통 사전 점검
- 1 · 승격
- 두 피연산자는 정수형 또는 범위 없는 열거형이며 정수 승격을 거칩니다.
- 2 · 횟수
0 ≤ n < N. 음수이거나 승격된 왼쪽 타입의 너비N이상이면 정의되지 않습니다.- 3 · 결과
- 결과 타입은 승격된 왼쪽 피연산자
E1의 타입입니다.
E1 << n · 왼쪽 시프트
빈 낮은 자리는 0으로 채웁니다.
- 값 계약
E1 × 2ⁿ과2ᴺ을 법으로 합동인 결과를 만듭니다.- 32비트 unsigned 예
10u << 2→40u
E1 >> n · 오른쪽 시프트
낮은 자리 비트를 버리고 현재 표준의 나눗셈 규칙을 적용합니다.
- 값 계약
E1 / 2ⁿ을 음의 무한대 방향으로 반올림합니다.- signed 예
-5 >> 1→-3· 부호 확장
E1은 n보다 먼저 평가됩니다. 시프트를 성능 요령으로 선택하지 말고, 고정 너비의 비트 이동이 의도일 때 사용합니다.
마스크와 플래그
마스크는 관심 있는 자리를 1로 표시한 값입니다. 서로 겹치지 않는 단일 플래그는 보통 1u << n 형태로 정의합니다.
#include <cstdint>
using Flags = std::uint32_t;
constexpr Flags READ = Flags{1} << 0;
constexpr Flags WRITE = Flags{1} << 1;
constexpr Flags DEBUG = Flags{1} << 2;
Flags flags = READ | DEBUG; // 0101
flags |= WRITE; // 설정: 0111
flags &= static_cast<Flags>(~READ); // 해제: 0110
flags ^= DEBUG; // 토글: 0010
bool has_any = (flags & (READ | WRITE)) != 0;
bool has_all = (flags & (READ | WRITE)) == (READ | WRITE);(flags & mask) != 0은 마스크 중 하나 이상이 켜졌는지 묻고, (flags & mask) == mask는 마스크의 모든 비트가 켜졌는지 묻습니다. 단일 비트 마스크에서는 두 질문의 결과가 같지만, 여러 비트를 합친 마스크에서는 다릅니다.
~mask도 승격 규칙의 영향을 받습니다. 좁은 부호 없는 타입은 ~ 전에 더 넓은 int로 승격될 수 있으므로, 반전 결과를 static_cast<Flags>로 되돌리면 마스크에 적용할 너비가 분명해집니다.
FLAG MASK LEDGER
마스크가 가리키는 비트를 어떻게 바꿀지와, 검사 결과를 어떤 조건으로 판정할지를 따로 읽습니다.
타입과 비트 배치 계약
flags와 mask는 필요한 너비를 가진 같은 unsigned 타입으로 둡니다.
좁은 unsigned도 ~ 전에 승격될 수 있으므로 static_cast<Flags>(~mask)로 선택한 너비에 되돌립니다.
READ=0001 · WRITE=0010 · DEBUG=0100 · 현재 flags=0101
| 목적 | 식 · 마스크 의미 · flags=0101 예 |
|---|---|
| 설정 | flags |= mask — 1인 자리를 켭니다.예: |
| 해제 | flags &= static_cast<Flags>(~mask) — 승격 후 반전한 값을 Flags 너비로 되돌려 해당 자리만 0으로 만듭니다.예: |
| 토글 | flags ^= mask — 1인 자리의 상태를 반전합니다.예: |
| 하나라도 확인 | (flags & mask) != 0 — 겹치는 비트가 하나 이상인지 봅니다.예: |
| 모두 확인 | (flags & mask) == mask — 요구한 비트가 전부 켜졌는지 봅니다.예: |
상태 변경
|= mask- 설정 ·
0101 | 0010 → 0111 &= static_cast<Flags>(~mask)- 해제 · 선택한 너비에서
0101 & ~0001 → 0100 ^= mask- 토글 ·
0101 ^ 0100 → 0001
조건 검사
(flags & mask) != 0- 하나라도 켜졌는지 확인 ·
0101 & 0011 → true (flags & mask) == mask- 모두 켜졌는지 확인 ·
0101 & 0011 → false
단일 비트 마스크에서는 any와 all이 같아 보이지만, 결합 마스크에서는 결과가 달라집니다. 여러 실행 흐름이 같은 값을 바꾸면 별도의 동기화나 원자 연산이 필요합니다.
여러 스레드가 같은 플래그 값을 동시에 바꾸는 경우에는 비트 식만으로 원자성이 생기지 않습니다. 해당 공유 상태에는 원자 연산이나 락 같은 별도의 동기화 계약이 필요합니다.
XOR의 가역성과 보안 경계
x ^ key ^ key == x이므로 같은 키를 두 번 XOR하면 원래 값으로 돌아옵니다.
#include <cstdint>
std::uint8_t original = 0x58;
std::uint8_t key = 0x0f;
std::uint8_t transformed = static_cast<std::uint8_t>(original ^ key);
std::uint8_t restored = static_cast<std::uint8_t>(transformed ^ key);이 성질만으로 암호화가 되지는 않습니다. 고정 키 XOR은 패턴과 키를 쉽게 노출할 수 있으므로, 위 코드는 가역 변환을 설명하는 예제일 뿐 보안 기능으로 사용하면 안 됩니다.
우선순위와 괄호
관련 연산자의 우선순위는 높은 쪽부터 대략 다음과 같습니다.
- 단항
~,! - 산술 연산자
- 시프트
<<,>> - 관계·동등 연산자
- 비트 AND
& - 비트 XOR
^ - 비트 OR
| - 논리 AND
&&, 논리 OR|| - 대입과 복합 대입
특히 flags & READ != 0은 원하는 검사로 묶이지 않습니다. 동등 연산자가 비트 AND보다 우선하므로 다음처럼 괄호로 마스크 계산을 먼저 드러내세요.
if ((flags & READ) != 0) {
// READ 비트가 켜져 있음
}우선순위는 구문이 묶이는 방법을 정하지만, 일반적인 피연산자 평가 순서를 모두 정하는 규칙은 아닙니다. 비트 연산 코드에서는 타입, 너비, 마스크 의미와 괄호를 함께 검토하세요.