C++ Bit Manipulation | Complete 'Bitmask' Guide
이 글의 핵심
Bit operations (AND, OR, XOR, shift) are low-level techniques used for flags, bitmasks, and algorithm optimization. This guide explains operator meanings, bitset usage, and common undefined behavior and portability tips with C++ examples.
Introduction
Bit-level operations are frequently used to represent flag sets as a single integer or compress state in algorithms. This guide clarifies operator meanings, then helps you apply examples and production tips to your own code.
Bit operations manipulate integers at the bit level. Used for flag management, memory optimization, and algorithm optimization, they execute in a single CPU cycle and are extremely fast.
Reality in Production
When learning development, everything seems clean and theoretical. But production is different. Wrestling with legacy code, chasing tight deadlines, facing unexpected bugs. The content covered here was initially learned as theory, but through applying it to real projects, I realized “ah, that’s why it’s designed this way.”
Particularly memorable are the trials and errors from my first project. I followed what I learned from books but couldn’t figure out why it didn’t work, spending days stuck. Eventually discovered the problem through senior developer code review, learning a lot in the process. This guide covers not just theory but also pitfalls and solutions you might encounter in practice.
1. Bit Operators
Basic Operators
Here is the main implementation:
#include <iostream>
#include <bitset>
int main() {
// 0b: binary literal (C++14)
int a = 0b1010; // 10 (binary 1010)
int b = 0b1100; // 12 (binary 1100)
// std::bitset<4>: output as 4-bit binary
std::cout << "a: " << std::bitset<4>(a) << " (" << a << ")" << std::endl;
std::cout << "b: " << std::bitset<4>(b) << " (" << b << ")" << std::endl;
std::cout << std::endl;
// AND (&): 1 if both 1, else 0
// 1010 & 1100 = 1000 (8)
std::cout << "a & b: " << std::bitset<4>(a & b) << " (" << (a & b) << ")" << std::endl;
// OR (|): 1 if either 1, 0 if both 0
// 1010 | 1100 = 1110 (14)
std::cout << "a | b: " << std::bitset<4>(a | b) << " (" << (a | b) << ")" << std::endl;
// XOR (^): 1 if different, 0 if same
// 1010 ^ 1100 = 0110 (6)
std::cout << "a ^ b: " << std::bitset<4>(a ^ b) << " (" << (a ^ b) << ")" << std::endl;
// NOT (~): bit inversion (0→1, 1→0)
// ~1010 = ...11110101 (negative, two's complement)
std::cout << "~a: " << std::bitset<8>(~a) << " (" << (~a) << ")" << std::endl;
// Left shift (<<): shift bits left (×2)
// 1010 << 1 = 10100 (20)
std::cout << "a << 1: " << std::bitset<8>(a << 1) << " (" << (a << 1) << ")" << std::endl;
// Right shift (>>): shift bits right (÷2)
// 1010 >> 1 = 0101 (5)
std::cout << "a >> 1: " << std::bitset<8>(a >> 1) << " (" << (a >> 1) << ")" << std::endl;
return 0;
}
Output:
Run the following commands:
a: 1010 (10)
b: 1100 (12)
a & b: 1000 (8)
a | b: 1110 (14)
a ^ b: 0110 (6)
~a: 11110101 (-11)
a << 1: 00010100 (20)
a >> 1: 00000101 (5)
Operator Summary
| Operator | Name | Description | Example |
|---|---|---|---|
& | AND | 1 if both 1 | 1010 & 1100 = 1000 |
| | OR | 1 if either 1 | 1010 | 1100 = 1110 |
^ | XOR | 1 if different | 1010 ^ 1100 = 0110 |
~ | NOT | Bit inversion | ~1010 = ...0101 |
<< | Left Shift | Shift left (×2) | 1010 << 1 = 10100 |
>> | Right Shift | Shift right (÷2) | 1010 >> 1 = 0101 |
2. Bitmasks
Flag Management
Here is the main implementation:
#include <iostream>
// Flag definition: use each bit as one flag
// 1 << n: shift 1 left n times (only nth bit is 1)
const int FLAG_READ = 1 << 0; // 0b0001 = 1 (0th bit)
const int FLAG_WRITE = 1 << 1; // 0b0010 = 2 (1st bit)
const int FLAG_EXEC = 1 << 2; // 0b0100 = 4 (2nd bit)
const int FLAG_DELETE = 1 << 3; // 0b1000 = 8 (3rd bit)
// Each flag uses different bit (no overlap)
int main() {
int permissions = 0; // Initial permission: none (0b0000)
// Set flag (OR): set specific bit to 1
// |=: OR then assign
permissions |= FLAG_READ; // 0b0000 | 0b0001 = 0b0001
permissions |= FLAG_WRITE; // 0b0001 | 0b0010 = 0b0011
std::cout << "Permissions: " << permissions << std::endl; // 3 (0b0011)
// Check flag (AND): check if specific bit is 1
// &: AND operation (extract that bit only)
if (permissions & FLAG_READ) { // 0b0011 & 0b0001 = 0b0001 (true)
std::cout << "Read allowed" << std::endl;
}
if (permissions & FLAG_EXEC) { // 0b0011 & 0b0100 = 0b0000 (false)
std::cout << "Execute allowed" << std::endl;
} else {
std::cout << "Execute denied" << std::endl;
}
// Clear flag (AND NOT): set specific bit to 0
// ~FLAG_WRITE: 0b0010 → 0b...11111101 (bit inversion)
// &=: AND then assign
permissions &= ~FLAG_WRITE; // 0b0011 & 0b...11111101 = 0b0001
std::cout << "After clearing write: " << permissions << std::endl; // 1 (0b0001)
// Toggle flag (XOR): invert specific bit (0→1, 1→0)
// ^=: XOR then assign
permissions ^= FLAG_EXEC; // 0b0001 ^ 0b0100 = 0b0101
std::cout << "After toggling exec: " << permissions << std::endl; // 5 (0b0101)
return 0;
}
Output:
Run the following commands:
Permissions: 3
Read allowed
Execute denied
After clearing write: 1
After toggling exec: 5
3. Practical Examples
Example 1: Permission System
Here is the grant implementation:
#include <iostream>
#include <string>
enum Permission {
PERM_READ = 1 << 0, // 0b0001
PERM_WRITE = 1 << 1, // 0b0010
PERM_EXEC = 1 << 2, // 0b0100
PERM_DELETE = 1 << 3 // 0b1000
};
class File {
int permissions = 0;
std::string name;
public:
File(const std::string& n) : name(n) {}
void grant(int perm) {
permissions |= perm;
}
void revoke(int perm) {
permissions &= ~perm;
}
bool has(int perm) const {
return (permissions & perm) == perm;
}
void printPermissions() const {
std::cout << "File: " << name << std::endl;
std::cout << " Read: " << (has(PERM_READ) ? "O" : "X") << std::endl;
std::cout << " Write: " << (has(PERM_WRITE) ? "O" : "X") << std::endl;
std::cout << " Execute: " << (has(PERM_EXEC) ? "O" : "X") << std::endl;
std::cout << " Delete: " << (has(PERM_DELETE) ? "O" : "X") << std::endl;
}
};
int main() {
File file("test.txt");
// Grant read/write permissions
file.grant(PERM_READ | PERM_WRITE);
file.printPermissions();
std::cout << std::endl;
// Revoke write permission
file.revoke(PERM_WRITE);
file.printPermissions();
return 0;
}
Output:
Run the following commands:
File: test.txt
Read: O
Write: O
Execute: X
Delete: X
File: test.txt
Read: O
Write: X
Execute: X
Delete: X
Example 2: Bit Tricks
Here is the isEven implementation:
#include <iostream>
// Even/odd check
bool isEven(int n) {
return (n & 1) == 0;
}
// Power of two check
bool isPowerOfTwo(int n) {
return n > 0 && (n & (n - 1)) == 0;
}
// Bit count (number of 1s)
int countBits(int n) {
int count = 0;
while (n) {
count += n & 1;
n >>= 1;
}
return count;
}
// Extract lowest bit
int lowestBit(int n) {
return n & -n;
}
// Highest bit position
int highestBitPos(int n) {
int pos = 0;
while (n >>= 1) ++pos;
return pos;
}
// Set bit
int setBit(int n, int pos) {
return n | (1 << pos);
}
// Clear bit
int clearBit(int n, int pos) {
return n & ~(1 << pos);
}
// Toggle bit
int toggleBit(int n, int pos) {
return n ^ (1 << pos);
}
// Test bit
bool testBit(int n, int pos) {
return (n & (1 << pos)) != 0;
}
int main() {
std::cout << "isEven(10): " << isEven(10) << std::endl; // 1
std::cout << "isEven(11): " << isEven(11) << std::endl; // 0
std::cout << "isPowerOfTwo(16): " << isPowerOfTwo(16) << std::endl; // 1
std::cout << "isPowerOfTwo(15): " << isPowerOfTwo(15) << std::endl; // 0
std::cout << "countBits(0b1011): " << countBits(0b1011) << std::endl; // 3
std::cout << "lowestBit(0b1010): " << lowestBit(0b1010) << std::endl; // 2
std::cout << "highestBitPos(0b1000): " << highestBitPos(0b1000) << std::endl; // 3
int n = 0b1010;
std::cout << "setBit(n, 0): " << std::bitset<4>(setBit(n, 0)) << std::endl; // 1011
std::cout << "clearBit(n, 1): " << std::bitset<4>(clearBit(n, 1)) << std::endl; // 1000
std::cout << "toggleBit(n, 2): " << std::bitset<4>(toggleBit(n, 2)) << std::endl; // 1110
std::cout << "testBit(n, 3): " << testBit(n, 3) << std::endl; // 1
return 0;
}
4. std::bitset
Basic Usage
Here is the main implementation:
#include <bitset>
#include <iostream>
int main() {
// Creation
std::bitset<8> bits1("10101010");
std::bitset<8> bits2(170); // 10101010
std::cout << "bits1: " << bits1 << std::endl;
std::cout << "bits2: " << bits2 << std::endl;
// Bit manipulation
bits1.set(0); // 10101011
bits1.reset(1); // 10101001
bits1.flip(2); // 10101101
std::cout << "After modification: " << bits1 << std::endl;
// Bit check
if (bits1.test(0)) {
std::cout << "Bit 0 is set" << std::endl;
}
// Count
std::cout << "Set bits: " << bits1.count() << std::endl;
// All/none check
std::cout << "All set: " << bits1.all() << std::endl;
std::cout << "Any set: " << bits1.any() << std::endl;
std::cout << "None set: " << bits1.none() << std::endl;
// Conversion
std::cout << "Integer: " << bits1.to_ulong() << std::endl;
std::cout << "String: " << bits1.to_string() << std::endl;
return 0;
}
Output:
Run the following commands:
bits1: 10101010
bits2: 10101010
After modification: 10101101
Bit 0 is set
Set bits: 5
All set: 0
Any set: 1
None set: 0
Integer: 173
String: 10101101
5. Bit Fields
Struct Bit Fields
Here is the main implementation:
#include <iostream>
struct Flags {
unsigned int read : 1;
unsigned int write : 1;
unsigned int exec : 1;
unsigned int reserved : 29;
};
int main() {
Flags f = {1, 0, 1, 0};
std::cout << "Read: " << f.read << std::endl; // 1
std::cout << "Write: " << f.write << std::endl; // 0
std::cout << "Execute: " << f.exec << std::endl; // 1
std::cout << "Size: " << sizeof(f) << " bytes" << std::endl; // 4 bytes
// Modification
f.write = 1;
f.exec = 0;
std::cout << "\nAfter modification:" << std::endl;
std::cout << "Read: " << f.read << std::endl; // 1
std::cout << "Write: " << f.write << std::endl; // 1
std::cout << "Execute: " << f.exec << std::endl; // 0
return 0;
}
6. Common Issues
Issue 1: Signed Shift
Here is the main implementation:
#include <iostream>
int main() {
// ❌ Negative right shift (implementation-defined)
int x = -8;
int y = x >> 1; // -4 (arithmetic shift) or other value
std::cout << "Negative shift: " << y << std::endl;
// ✅ Unsigned type
unsigned int ux = 8;
unsigned int uy = ux >> 1; // 4 (logical shift, guaranteed)
std::cout << "Unsigned shift: " << uy << std::endl;
return 0;
}
Issue 2: Shift Overflow
Here is the main implementation:
#include <iostream>
int main() {
// ❌ Shift into sign bit (undefined behavior)
int x = 1 << 31; // UB
std::cout << "Signed shift: " << x << std::endl;
// ✅ Unsigned type
unsigned int ux = 1u << 31; // OK
std::cout << "Unsigned shift: " << ux << std::endl;
return 0;
}
Issue 3: Bit Field Portability
Here is the main implementation:
#include <iostream>
// ❌ Bit order undefined (compiler-dependent)
struct BadFlags {
unsigned int a : 1;
unsigned int b : 1;
unsigned int c : 1;
};
// ✅ Use bitmask (portable)
const unsigned int FLAG_A = 1 << 0;
const unsigned int FLAG_B = 1 << 1;
const unsigned int FLAG_C = 1 << 2;
int main() {
unsigned int flags = 0;
flags |= FLAG_A;
flags |= FLAG_C;
std::cout << "Flags: " << flags << std::endl; // 5 (0b101)
return 0;
}
7. Practical Example: Bitmask DP
Here is the solve implementation:
#include <iostream>
#include <vector>
#include <algorithm>
// Traveling Salesman Problem (TSP) - Bitmask DP
class TSP {
int n;
std::vector<std::vector<int>> dist;
std::vector<std::vector<int>> dp;
public:
TSP(const std::vector<std::vector<int>>& distances)
: n(distances.size()), dist(distances) {
dp.assign(1 << n, std::vector<int>(n, -1));
}
int solve(int visited, int current) {
// All visited
if (visited == (1 << n) - 1) {
return dist[current][0]; // Back to start
}
// Memoization
if (dp[visited][current] != -1) {
return dp[visited][current];
}
int minCost = 1e9;
// Choose next city
for (int next = 0; next < n; ++next) {
// Not yet visited
if (!(visited & (1 << next))) {
int cost = dist[current][next] +
solve(visited | (1 << next), next);
minCost = std::min(minCost, cost);
}
}
return dp[visited][current] = minCost;
}
int findMinPath() {
return solve(1, 0); // Start from city 0
}
};
int main() {
std::vector<std::vector<int>> dist = {
{0, 10, 15, 20},
{10, 0, 35, 25},
{15, 35, 0, 30},
{20, 25, 30, 0}
};
TSP tsp(dist);
std::cout << "Minimum path cost: " << tsp.findMinPath() << std::endl;
return 0;
}
Summary
Key Takeaways
- Bit operators: &, |, ^, ~, <<, >>
- Bitmasks: Flag management (set, clear, check, toggle)
- Bit fields: Memory optimization (low portability)
- bitset: Convenient bit manipulation
- Bit tricks: Even check, power of two, bit count
- Performance: Single CPU cycle (very fast)
Bit Operation Usage
| Usage | Technique | Example |
|---|---|---|
| Set flag | |= | flags |= FLAG_READ |
| Clear flag | &= ~ | flags &= ~FLAG_WRITE |
| Check flag | & | if (flags & FLAG_EXEC) |
| Toggle flag | ^= | flags ^= FLAG_DELETE |
| Even check | & 1 | if ((n & 1) == 0) |
| Power of two | & (n-1) | if (n & (n-1) == 0) |
Practical Tips
Usage principles:
- Flag management: bitmasks
- Memory optimization: bit fields
- Convenient manipulation:
bitset - Algorithms: bit tricks
Performance:
- Bit operations are very fast (1 cycle)
- Compiler auto-optimizes
bitsetslightly slower (convenience tradeoff)- Bitmask DP optimizes to O(2ⁿ·n)
Cautions:
- Signed shift is implementation-defined
- Watch for shift overflow
- Bit fields have low portability
- Consider readability (add comments)
Next Steps
- C++ Bitset
- C++ Bit Operations
- Algorithm Bitmask DP
Related Articles
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Bit operations (AND, OR, XOR, shift) are low-level techniques used for flags, bitmasks, and algorithm optimization. This… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ Algorithm Heap | ‘힙 알고리즘’ 가이드
- C++ Exception Specifications | ‘예외 명세’ 가이드
- C++ noexcept 지정자 | ‘예외 명세’ 가이드
이 글에서 다루는 키워드 (관련 검색어)
C++, bitwise, bitmask, bit manipulation 등으로 검색하시면 이 글이 도움이 됩니다.