整数

Basis
最終更新: タグ: Types

変数の基本 チェックポイントでは整数型を Zig の基本的な構成要素の一つとして紹介し、算術、オーバーフロー挙動、型キャストの詳細な解説を約束した。このチェックポイントでその約束を果たす。最終的には、どんな状況でも適切な整数型を選び、任意の基数で数値定数を書き、算術が境界に達したときに何が起こるかを理解し、異なる整数型間で値を安全に移動できるようになる。

整数型の一覧

命名パターンはすでに知っている:プレフィックス u は符号なし(unsigned)、i は符号あり(signed)を意味し、続く数字がビット幅(bit width)— 型がメモリ上で占める二進数桁数だ。

前のチェックポイントで触れなかったのは、Zig が固定されたビット幅のメニューに限定されないということだ。0 から 65535 までの任意のビット幅が使える。u1i3u7u24u48u128 のような型はすべて完全に有効だ。日常のコードでは、ほぼ常に 2 の累乗の標準的なビット幅を使う:

符号ビット幅値の範囲
u8なし80 〜 255
i8あり8−128 〜 127
u16なし160 〜 65 535
i16あり16−32 768 〜 32 767
u32なし320 〜 4 294 967 295
i32あり32−2 147 483 648 〜 2 147 483 647
u64なし640 〜 約 1.8 × 10¹⁹
i64あり64約 ±9.2 × 10¹⁸
u128なし1280 〜 約 3.4 × 10³⁸
i128あり128約 ±1.7 × 10³⁸

プラットフォーム依存の二つの型が全体像を完成させる:

  • usize — 現在のプラットフォームのメモリアドレスと同じ幅の符号なし整数:32 ビットシステムでは 32 ビット、64 ビットシステムでは 64 ビット。Zig は配列インデックス、スライスの長さ、バイト数を表す値に usize を使う。
  • isizeusize の符号あり対応型。二つのインデックスやアドレスの差が負になる可能性がある場合に必要だ。

ビット幅の選び方: 迷ったときは i32 または u32 でほとんどの汎用算術をカバーできる。バイトサイズの値(カラーチャンネル、ASCII コード)には u8、潜在的に大きな値には u64/i64、メモリサイズや配列インデックスに関係するものには usize を使う。

整数リテラルの書き方

整数リテラル(integer literal)はソースコードに直接書かれた数値だ。Zig は四つの基数をサポートする:

プレフィックス基数十進数での値
(なし)10 — 十進数255255
0x16 — 十六進数0xFF255
0o8 — 八進数0o377255
0b2 — 二進数0b1111_1111255

これらの四つのリテラルはすべて同じ値を表す — 意図が最も明確になる基数を選ぼう。十六進数はカラー値、メモリアドレス、4 ビット単位で考えるプロトコルフィールドに自然だ。二進数はマスクやビットフラグに最も明確だ。

アンダースコアはリテラルの内部のどこにでも現れられる(先頭と末尾を除く)が、コンパイラーは静かに無視する。桁を読みやすいかたまりにグループ化するために自由に使おう:

const red:   u32 = 0xFF_00_00;        // RGB カラー:赤が最大、緑と青はゼロ
const mask:  u8  = 0b0000_1111;       // 下位ニブル:ビット 0〜3 がセット、4〜7 がクリア
const limit: u64 = 1_000_000_000_000; // 1 兆、1000000000000 より見やすい

明示的な型アノテーションがないリテラルは特別なコンパイル時型 comptime_int を持ち、任意精度で値を格納してオーバーフローしない。型付きの変数に代入した瞬間に、コンパイラーはそれを絞り込んで値が収まるか確認する:

const x = 9_999_999_999; // comptime_int — 問題なし、まだオーバーフローの可能性なし
const y: u32 = 9_999_999_999; // コンパイルエラー:値が u32 に収まらない

このチェックはコンパイル時に行われるため、整数リテラルが静かにオーバーフローすることはない — 範囲外のリテラルは常にコンパイルエラーになる。

算術演算

Zig は整数に対して標準的な算術演算子を提供する:

演算子演算
+加算
-減算(二項)または符号反転(単項)
*乗算
/除算
%剰余

二項演算の両方のオペランドは同じ型でなければならない。 Zig は一方を暗黙的に拡大または縮小して他方に合わせることはない。型の混在はコンパイルエラーになる:

const a: u32 = 100;
const b: u64 = 200;
const c = a + b; // コンパイルエラー:型の不一致 — u32 と u64 は異なる型

整数除算はゼロ方向に切り捨てる — 小数部は丸められずに捨てられる:

const q1 = 7  / 2;  // 3  (3.5 でも 4 でもない)
const q2 = -7 / 2;  // -3 (ゼロ方向に切り捨て、-4 ではない)

% 演算子は除算後の剰余を返す。符号は常に被除数(左側のオペランド)に従う:

const r1 =  7 % 3;  //  1
const r2 = -7 % 3;  // -1  (符号は除数 3 ではなく被除数 −7 に従う)

Zig は二進数表現を直接操作するビット演算子も提供する:

演算子演算
&ビット AND
|ビット OR
^ビット XOR
~ビット NOT(単項)
<<左シフト
>>右シフト

これらはハードウェアレジスター、ネットワークプロトコル、画像データ、または個々のビットが独立した意味を持つ任意のコンテキストを扱うときに最も有用だ。

オーバーフローと Zig の安全性モデル

オーバーフロー(overflow)は算術結果が型の保持できる範囲外になるときに発生する。すでに 255 を含む u8 に 1 を足すと 256 になるが、8 ビットには収まらない。

Zig がこれをどう扱うかはコンパイルするときのビルドモード(build mode)に依存する:

  • Debugzig run または zig build をフラグなしで実行したときのデフォルト)と ReleaseSafe — オーバーフローはランタイムパニック(runtime panic)を引き起こす。プログラムは即座に停止してエラーを出力する。これは意図的だ:Zig は本番環境で静かにではなく、開発中に大きな音でオーバーフローのバグを見つけさせたい。
  • ReleaseFastReleaseSmall — オーバーフローチェックは速度向上やバイナリサイズ削減のために除去される。結果として生じる挙動は未定義であり、正確性を頼りにすることはできない。

ラッピング算術

モジュラーラップアラウンドが意図した挙動になる場合がある — 例えば、循環カウンターや特定の暗号アルゴリズムだ。ビルドモード依存の挙動に頼るのではなく、ビルドモードに関係なく常にラップする Zig のラッピング演算子(wrapping operators)を使う:

演算子演算
+%ラッピング加算
-%ラッピング減算
*%ラッピング乗算
const std = @import("std");

pub fn main() void {
    const max: u8  = 255;
    const next: u8 = max +% 1; // あらゆるビルドモードで常に 0

    std.debug.print("255 +% 1 = {}\n", .{next}); // 255 +% 1 = 0
}

飽和算術

ラップアラウンドが無意味な結果を生むが、パニックも望ましくない場合 — クランプされた設定の調整、上限を超えてはならない累積合計など — 飽和演算子(saturating operators)を使う。型の最小値または最大値に結果をクランプする:

演算子演算
+|飽和加算
-|飽和減算
*|飽和乗算
const std = @import("std");

pub fn main() void {
    const vol:   u8 = 250;
    const louder: u8 = vol +| 10; // 255 — 最大値に留まり、4 にラップしない

    const low:    i8 = -120;
    const lower:  i8 = low -| 20; // -128 — 最小値に留まる

    std.debug.print("louder = {}\n", .{louder}); // louder = 255
    std.debug.print("lower  = {}\n", .{lower});  // lower  = -128
}

原則として:モジュラー算術が意図のときはラッピング演算子を使い、クランプが意図のときは飽和演算子を使い、オーバーフローがロジックのバグを意味するときは通常の演算子(+-*)を使う。

整数型間のキャスト

Zig は整数型間の暗黙的な変換を一切行わない — すべての変換は明示的に書かなければならない。三つのビルトイン関数がこれを処理する。

@as による型の拡大

@as(T, value)value を型 T に強制変換する。型の拡大(widening)— 小さい型から大きい型へ — はこれで常に安全であり、コンパイル時に受け入れられる:

const byte: u8  = 200;
const word: u32 = @as(u32, byte); // 常に安全:200 は常に u32 に収まる

@as は、周囲のコンテキストがまだ指定していないときにリテラルに特定の型を強制する標準的な方法でもある:

const flags = @as(u16, 0b0000_0001_0000_0000);

@intCast による型の縮小

@intCast(value) は整数を周囲のコンテキストが期待する型に変換する。より狭い型へ変換するとき、または符号の変更を行うとき — 値が収まらない可能性がある状況で使う。Debug と ReleaseSafe ビルドでは、ソース値が変換先の型に収まらない場合、@intCastランタイムパニックを引き起こす:

const big:   i64 = 1_000;
const small: i32 = @intCast(big); // 問題なし:1 000 は i32 に収まる

const too_big: i64 = 3_000_000_000;
const bad:     i32 = @intCast(too_big); // Debug/ReleaseSafe ではランタイムパニック

@intCast を書くことは主張だ:「この値はターゲット型に収まることを知っている。」セーフビルドでのランタイムチェックはその主張を強制し、間違いを捉える。

@truncate によるビット切り捨て

@truncate(value) は下位ビットだけを保持して残りを捨てることで、値を変換先の型に収める。パニックは発生しない:

const word: u16 = 0x1234;
const low:  u8  = @truncate(word); // 0x34 — 下位 8 ビットを保持し、0x12 を捨てる

@truncate は上位ビットを意図的に捨てるときだけ使う。上位ビットがゼロである(つまり値が実際に収まる)という保証が欲しい場合は、代わりに @intCast を使う — セーフビルドでそうでない場合にパニックする。

適切なツールの選び方

状況ツール
より大きな型へ拡大する@as(T, value)
縮小する、値が収まることのランタイムチェックが欲しい@intCast(value)
縮小する、意図的に上位ビットを捨てる@truncate(value)

まとめ

  • Zig の整数型は符号(u/i)とビット幅を名前に直接エンコードする。0 から 65535 までの任意のビット幅を選べる;最もよく使われるのは 8、16、32、64、128。usizeisize はインデックス、長さ、メモリサイズに使われるプラットフォームサイズの型だ。
  • 整数リテラルは十進数、十六進数(0x)、八進数(0o)、二進数(0b)で書ける。リテラル内部のどこにでもアンダースコアを入れて可読性を高められる。裸のリテラルは型 comptime_int — 任意精度でコンパイル時のみ — を持ち、代入されたときに具体的な型に絞り込まれる。
  • 算術演算子(+-*/%)は両方のオペランドが同じ型であることを要求する。整数除算はゼロ方向に切り捨てる。% 剰余の符号は被除数に従う。
  • オーバーフローは Debug と ReleaseSafe ビルドではランタイムパニックを引き起こす;ReleaseFast と ReleaseSmall ではチェックが除去される。意図的なモジュラー算術にはラッピング演算子(+%-%*%)を使い、意図的なクランプには飽和演算子(+|-|*|)を使う — どちらもすべてのビルドモードで一貫して動作する。
  • 三つのキャストビルトイン:拡大には @as(T, v)、ランタイム安全チェック付きの縮小には @intCast(v)、チェックなしの意図的なビット切り捨てには @truncate(v)