ビット演算 — フラグをビットで持つ
ファイルの権限 755、TCP のフラグ、正規表現のオプション。これらはどれも複数の真偽値を 1 つの整数に詰め込んだものです。詰め込みと取り出しを担うのがビット演算です。
日常の Web 開発で毎日書くものではありませんが、既存のコードやプロトコルの仕様を読むときに必ず出てきます。
この章で学ぶこと
- ビット演算子それぞれが何をするか
- 複数のフラグを 1 つの整数で管理する方法
- フラグの判定・追加・削除・反転の定型
- JavaScript のビット演算が 32 ビット符号付きで動くこと
2 進数の読み書きができると理解が早くなります。本章の中でも簡単に触れます。
整数はビットの並び
コンピュータは整数を 2 進数で持ちます。8 ビットなら次のような対応です。
| 10 進 | 2 進 (8 ビット) |
|---|---|
| 1 | 00000001 |
| 2 | 00000010 |
| 4 | 00000100 |
| 5 | 00000101 |
| 8 | 00001000 |
右端が 1 の位、その左が 2 の位、次が 4 の位と、左へ行くほど 2 倍になります。桁の位置を「ビット 0」「ビット 1」と右から数えるのが慣例です。5 は 00000101 なので、ビット 0 とビット 2 が立っています。
演算子
& — AND (論理積)
両方のビットが 1 のときだけ 1 になります。
00000101 (5)
& 00000011 (3)
------------
00000001 (1)
特定のビットが立っているかを調べるのに使います。
| — OR (論理和)
どちらかが 1 なら 1 になります。ビットを立てるのに使います。
00000101 (5)
| 00000010 (2)
------------
00000111 (7)
^ — XOR (排他的論理和)
片方だけが 1 のときに 1 になります。ビットを反転させるのに使います。同じ値で 2 回 XOR すると元に戻る性質があります。
00000101 (5)
^ 00000110 (6)
------------
00000011 (3)
~ — NOT (ビット反転)
すべてのビットを反転します。ビットを落とすためのマスクを作るのに使います。
<< と >> — シフト
ビットの並びを左右にずらします。
1 << 0 → 00000001 (1)
1 << 1 → 00000010 (2)
1 << 2 → 00000100 (4)
1 << 3 → 00001000 (8)
**1 << n は「ビット n だけが立った値」**です。この形はビットフラグの基本になります。左シフトは 2 倍、右シフトは 2 で割る (小数切り捨て) のと同じ効果があります。
ビットフラグ
複数の真偽値を 1 つの整数で持つ手法です。それぞれの真偽値に別のビットを割り当てます。
const Permission = {
Read: 1 << 0, // 1 = 00000001
Write: 1 << 1, // 2 = 00000010
Delete: 1 << 2, // 4 = 00000100
Admin: 1 << 3, // 8 = 00001000
} as const;
値が 2 のべき乗になっているのがポイントです。それぞれが違う桁を使うので、足し合わせても情報が混ざりません。
判定する
/** value に flagBit が立っているか */
function hasFlag(value: number, flagBit: number): boolean {
return (value & flagBit) !== 0;
}
& で対象のビットだけを残し、0 でなければ立っています。
value = 5 (00000101) で試します。
| 呼び出し | 計算 | 結果 |
|---|---|---|
hasFlag(5, Permission.Read) | 00000101 & 00000001 = 00000001 | true |
hasFlag(5, Permission.Write) | 00000101 & 00000010 = 00000000 | false |
hasFlag(5, Permission.Delete) | 00000101 & 00000100 = 00000100 | true |
5 は「読み取りと削除は可、書き込みは不可」を表しています。
ビット番号を受け取る形で書かれていることもあります。中身は同じです。
/** value のビット位置 position が立っているか */
function isBitSet(value: number, position: number): boolean {
return (value & (1 << position)) !== 0;
}
追加・削除・反転
| 操作 | 書き方 | 仕組み |
|---|---|---|
| 立てる | value | flagBit | OR は 1 を残す |
| 落とす | value & ~flagBit | 対象だけ 0 のマスクと AND する |
| 反転する | value ^ flagBit | XOR は片方だけ 1 なら 1 |
| 複数まとめる | flagA | flagB | 別々の桁なので混ざらない |
let perms = Permission.Read | Permission.Write; // 3 = 00000011
perms = perms | Permission.Delete; // 7 = 00000111 (削除を許可)
perms = perms & ~Permission.Write; // 5 = 00000101 (書き込みを剥奪)
perms = perms ^ Permission.Admin; // 13 = 00001101 (管理者を反転)
hasFlag(perms, Permission.Write); // false
「落とす」で ~ を使うのが定型です。~Permission.Write は「Write のビットだけ 0 で、他はすべて 1」というマスクになるので、AND を取ると対象だけが消えます。
マスクで一部を取り出す
連続した複数ビットをまとめて取り出したいときは、その幅ぶんのマスクを作ります。
// 下位 4 ビットだけを取り出す
const lower4 = value & 0b1111; // 0b1111 は 15
// 上位側のビット 4 から 7 を取り出して右へ寄せる
const upper4 = (value >> 4) & 0b1111;
(1 << width) - 1 で「幅 width のマスク」が作れます。1 << 4 は 00010000 で、1 を引くと 00001111 になります。
何のために使うのか
| 用途 | 例 |
|---|---|
| 権限・オプションの集合 | ファイルパーミッション、機能フラグ |
| プロトコルのヘッダー | TCP のフラグ、IP のヘッダーフィールド |
| 状態の集合を省メモリで持つ | 大量のレコードの真偽値属性 |
| 高速な集合演算 | 和集合は OR、積集合は AND で 1 命令 |
真偽値を個別のカラムやプロパティで持つより、まとめて 1 つの整数で扱えるので比較や保存が軽くなります。複数条件の一致判定が 1 回の AND で済むのも利点です。
一方で可読性は落ちます。perms & 6 と書かれても意味は分かりません。定数に名前を付ける、判定を関数にまとめる、といった手当てが要ります。真偽値が数個で性能要件も無いなら、データ構造の選び方で見た Set やオブジェクトで持つほうが読みやすくなります。
JavaScript での注意点
JavaScript と TypeScript のビット演算には、他の言語と違う癖があります。
**オペランドは 32 ビット符号付き整数に変換されます。**MDN は「両方のオペランドを数値に変換したうえで、32 ビット整数へ変換してビット演算する」と記述しており、結果も 32 ビット整数です。表現は 2 の補数なので、扱える範囲は -2147483648 から 2147483647 です。
ここから 2 つの帰結があります。
// 1. 32 ビットを超える桁は捨てられる
const big = 2 ** 33; // 8589934592
console.log(big | 0); // 0 ← 上位ビットが失われる
// 2. ビット 31 を立てると負の数になる
console.log(1 << 31); // -2147483648
console.log(1 << 32); // 1 ← シフト量は 32 で一周する
**ビット 31 以上を使うフラグは素直に書けません。**33 個以上のフラグが要るなら BigInt を使うか、複数の数値に分けます。BigInt どうしのビット演算は 32 ビットへの切り詰めが起きません。
const flag40 = 1n << 40n; // BigInt なら桁あふれしない
なお、数値の型が uint8 や uint32 のように固定されている言語では、範囲を超えた分の扱い (切り捨て、あふれ) が言語ごとに決まっています。他言語のビット演算コードを読むときは、まずその型の幅と符号の有無を確認してください。
よくある誤解
「1 << 3 は 3」 — 8 です。1 << n は「ビット n だけが立った値」で、2 の n 乗になります。
「フラグの値は連番でよい」 — 2 のべき乗 (1・2・4・8) でなければ桁が重なって混ざります。3 は 00000011 なのでビット 0 とビット 1 の両方を占めます。
「JavaScript のビット演算は 64 ビットで動く」 — 32 ビット符号付きに変換されます。数値自体は 64 ビット浮動小数点ですが、ビット演算の前に切り詰められます。
「ビットフラグは常に効率的」 — 省メモリと高速判定と引き換えに可読性を失います。数個の真偽値ならオブジェクトのほうが適切です。
確認問題
問 1. 次のコードの出力を答えてください。
function isBitSet(value: number, position: number): boolean {
return (value & (1 << position)) !== 0;
}
const value = 10;
console.log(isBitSet(value, 0));
console.log(isBitSet(value, 1));
console.log(isBitSet(value, 3));
答え: 上から順に false / true / true
10 を 2 進数にすると 00001010 です。立っているのはビット 1 とビット 3 です。
| 呼び出し | 1 << position | AND の結果 | 出力 |
|---|---|---|---|
isBitSet(10, 0) | 00000001 | 00000000 = 0 | false |
isBitSet(10, 1) | 00000010 | 00000010 = 2 | true |
isBitSet(10, 3) | 00001000 | 00001000 = 8 | true |
10 = 8 + 2 なので、ビット 3 (8 の位) とビット 1 (2 の位) が立っている、と分解して考えると暗算できます。
問 2. 権限から「書き込み」だけを剥奪したいです。正しいのはどれですか。
const Write = 1 << 1; // 2
// A
perms = perms - Write;
// B
perms = perms & ~Write;
// C
perms = perms ^ Write;
// D
perms = perms | ~Write;
答え: B
- A (
- Write) — もともと書き込み権が無い場合に壊れます。perms = 5(00000101) から 2 を引くと3(00000011) になり、立っていなかったビット 1 が立ってしまいます - B (
& ~Write) —~Writeは「ビット 1 だけ 0、他はすべて 1」のマスクです。AND を取ると対象だけが消え、他は保たれます。元から立っていなくても結果は変わりません - C (
^ Write) — 反転なので、立っていなければ立ててしまいます。トグルであって剥奪ではありません - D (
| ~Write) — OR なので、ビット 1 以外がすべて立ちます。全権限付与に近い結果になります
「落とすときは & ~」が定型です。引き算で書くと状態に依存して壊れます。
問 3. 40 個のフラグを 1 つの数値で管理しようとしたら、うまく動きませんでした。なぜですか。
答え: JavaScript のビット演算が 32 ビット符号付き整数で行われるためです。
1 << 40 は期待どおりの値になりません。シフト量は 32 で一周するので 1 << 40 は 1 << 8 と同じ 256 になります。ビット 32 以上は表現できません。
さらにビット 31 を使った時点で結果が負の数になります (1 << 31 は -2147483648)。
対策は 2 つあります。
BigIntを使う —1n << 40nのように書けば切り詰めが起きません。ただし通常の数値と混ぜるとTypeErrorになります- 複数の数値に分ける — 32 個ずつのグループに分割して配列で持ちます
そもそもフラグが 40 個ある設計自体を見直す価値もあります。この規模なら Set やオブジェクトのほうが読みやすく、保守しやすくなります。
まとめ
&は判定、|は追加、^は反転、~はマスク作りに使います- **
1 << nは「ビット n だけが立った値」**で、ビットフラグの基本形です - フラグの値は 2 のべき乗にします。連番にすると桁が重なります
- 立てるのは
| flag、落とすのは& ~flag、反転は^ flagが定型です。引き算で落とすと状態に依存して壊れます - **JavaScript のビット演算は 32 ビット符号付きに変換されます。**ビット 31 以上は素直に扱えません
- 省メモリと高速判定の代わりに可読性を失います。数個の真偽値なら普通のオブジェクトが適切です
- MDN — ビット論理積 — 32 ビット整数への変換規則
- JavaScript ガイド — 演算子と数値の扱い
次に読む
- デザインパターン — 設計を共通の語彙で語る