
2026/07/09 4:35
常に署名されていません
RSS: https://news.ycombinator.com/rss
要約▶
日本語訳:
原文は強力ですが、流れを改善し、著者の C/C++ の振る舞いに対する見解と業界の慣行との区別をさらに明確にするために、若干微調整する余地があります。
改善された要約:
この文章では、符号付き整数演算が多くの場合危険かつ不必要であり、メモリ安全性と簡略化されたロジックを実現するためのより優れた代替手段として無符号型への移行を提唱しています。著者は、大多数の整数は負値を表さないにもかかわらず、それを符号付きとして扱うことは、有効な文脈において不可能な状態を暗示することで「null ポインタ」に似た複雑性を導入すると主張します。特に重要なのは、符号付き演算が頻繁に未定義動作(例:
INT_MIN / -1)やシステムパニックを引き起こすのに対し、無符号演算は減算による下のフロー(underflow)でも予測可能であり、巻き戻し最適化のために正しくコンパイルされる点です。Google C++ スタイルガイドのような業界ガイドは無符号型の使用を推奨していませんが、著者はこの助言が不安全な慣行を促進すると見なしています。その代わりに、開発者には Rust、Go、または Odin に見られるパターンを採用し、安全な逆方向反復を実現し、エラーの多いキャストを排除し、複雑なチェックなしに異なる場合を安全に処理すべきだと主張します。無符号演算を採用することで、防御的な範囲アサーションの必要性を取り払い、明確な値の範囲を通じて激しいコンパイラ最適化を可能にし、符号付きアンダーフローや無効な配列インデックスに関連する脆弱性を大幅に減少させることができます。本文
すべてを無符号整数へ:有符号整数がしばしば問題となる理由
著者: デール・ワイラー
最終更新日: 2022 年 1 月 1 日
概要
プログラム内の大多数の整数は実際には負の値を表せず、配列のインデックスやループ反復回数などでよく扱われる。したがって、有符号整数よりも無符号整数の利用頻度を高めることが望ましい。しかし現状では排他的に有符号整数を使用するという誤った選択が多く見られる。
本記事は主に C および C++ を焦点とするが、Go、Rust、Odin といった他の言語の例も示し、演算そのものの性質から議論を展開する。
無符号整数への反対意見に対する反論
セーフティに関する議論
- 最も一般的な批判は「無符号整数のアンダフロー(下回し)が溢れ(オーバーフロー)よりも容易で危険である」という点にある。
- しかし、これは誤った安堵を与える批判であり、不安定なコードを継続する方向を促す。
- 適切な慣用句を使用すれば、これらの問題は容易に回避可能である。
逆方向ループに関する議論
有符号整数に変換した誘惑から避けるための正しいアプローチは以下の通りである。
❌ 危険な有符号変換(NG)
// 幅の狭くなる変換を行い、正当な警告を無視するキャストを行っている for (int64_t i = int64_t(size) - 1; i >= 0; i--) { // ... }
- 問題:
のような値が入力されると、C/C++ では未定義の行動(Undefined Behavior)となり、ほぼ確実に攻撃対象となる。i >= 0x7fffffffffffffff - 批判の誤り: 「そのような値は病的である」という議論自体が誤りで、有符号整数を使用すれば無条件にアンダフローを呼び出し、未定義の行動を引き起こす。
✅ 正しいアプローチ(NG)
// 無符号アンダフローが良定义的であることを認識し、巻戻し演算を利用する for (size_t i = size - 1; i < size; i--) { // ... }
- 仕組み:
から始め、ゼロになった時点でアンダフローにより最大値に巻き戻る。その値はsize - 1
より大きいためsize
が偽となりループが終了する。i < size
Rust の例
Rust ではデバッグビルドでアンダフローをトラップ表現(trap representation)にする検討があるが、巻戻し振る舞いをクリーンな構文で安全に実現できるのが
[0..size).rev() などである。
// 無符号アンダフローをクリーンに処理 for i in (0..size).rev() { // ... }
- キャスト不要
- 「病的」な入力を正しく動作させる(
の範囲をカバー)[0, 0xffffffffffffffff)
でも機能する(ループ進入しないため)size == 0
2 つの数の差が負になる可能性があるという議論
$$ \Delta = x - y $$
- 批判:
の場合に無符号アンダフローが発生するため危険とされる。y > x - 反論: どちらのケースでも「有符号アンダフローを招く値」が存在する。C/C++ では有符号アンダフローが未定義であるため、無条件にエラーとなる。
- 解決策: 安全な差の計算は有符号で非常に難しい(
でさえ誤りとなる)。INT_MAX - INT_MIN
❌ 有符号での安全な実装(複雑)
if ((y > 0 && x < INT_MIN + y) || (y < 0 && x > INT_MAX + y)) { // エラー処理 } else { delta = abs(x - y); }
✅ 無符号での実装(簡単で安全)
delta = max(x, y) - min(x, y); // 常に安全に絶対値の差を与える
- 読みやすく、式自体が自己説明的であるため変数名の「delta」も不要。
符号付き演算によるインデックス計算の方が安全であるという議論
複雑な式でのインデックス計算において有符号を使用する主張は誤りであり、有符号整数著しく悪化する傾向がある。
区間の中間点計算例
バイナリ検索やマージソートなどで一般的に見られる
(low + high) / 2 は危険である。
int mid = (low + high) / 2; // 合計が INT_MAX を超えると負へオーバーフロー
- 有符号の解決策
:low + (high - low) / 2
の場合依然として失敗。low=INT_MIN, high=INT_MAX - 無符号の場合
:(low + high) / 2
で機能せず、正しい値low=0x80000000, high=0x80000002
が得られない。0x80000001
結論
- 有符号: メモリ安全性の問題を引き起こす(無効なインデックス)。
- 無符号: インデックスが間違っても有界の値であり、論理的バグに留まる。
✅ 正しい方法:巻戻しに対してマスク付き加算を使用
C++ の
std::midpoint が行うように、すべての変数を無符号に変換する。
template<typename T> T midpoint(T x, T y) { using U = std::make_unsigned<T>; // 型 T の無符号バージョン const auto digits = std::numeric_limits<T>::digits; U shift = digits - 1; U difference = (U)x - (U)y; U sign = y < x; // mask付き加算のロジック U half = (difference / 2) + (sign << shift) + (sign & difference); return (T)(x + half); }
無符号乗算が溢れる可能性があるという議論
malloc(sizeof(Object) * n) が溢ると、十分でないメモリを割り当ててしまう。
- 有符号の場合: 負の値は
にキャストされて約 4 GiB となり、静かにメモリ漏れを生じるリスクがある。size_t
✅ より良い解決策
C++:
std::make_unique<Object[]>(n); // 安全な割り当て // または auto arr = std::make_unique<Object[]>(n);
Go:
arr := make([]Object, n) // 自動オーバーフロー検出 arr := make([]Object, n, m) // コンストラクターによる安全な実装
チェックの簡易化
x * y が溢れるかチェックするテストはシンプルである:
if (x != 0 && y > ((T)-1) / x) { // オーバーフロー発生((T)-1 は UINT_MAX に相当) }
センチネル値
- 有符号整数での慣行: 「負の値=エラー」をエンコードする。これは字句的なカテゴリー誤りで、ひどいプログラミング実践である。
✅ 無符号整数でも安全な代替手段
- 結果用のアウトパラメータ
if (uint result; connect(&result)) { } - タプルまたはペアの返却
tuple<bool, uint> result = connect(); if (get<0>(result)) { ... } - オプション型(Rust)
match connect() { Some(u32) => // ... None => // エラー } - enum で全域を定義
enum class ConnectionStatus : unsigned { Connected, NoRouteToHost, Disconnected, TimedOut }; if (status == ConnectionStatus::Connected) { ... } - 複数の返却値(Go / Odin)
status, err := connect() if err != nil { ... }
デフォルトであるという議論
- C/C++ は
を有符号と定義し、これがデフォルトになった。しかしこれは多くのデフォルトが悪かったことに合意されている。int - プログラミング言語のデフォルトは例外ではなくルールと一致すべきである。
有符号整数を巻戻しするように定義したらどうでしょうか?
Go/Rust/Odin などで行われる「有符号巻戻し」への安全利用は限界がある。
- 問題: すべての配列アクセスで境界チェックが必要になり、実行時コストが発生する。維持が困難なためである。
- 未定義の行動リスト(LLVM ベース言語共通):
x / 0INT_MIN / -1INT_MAX % -1
これらは Go/Rust/Odin でさえ無効な結果を生じる。INT_MAX - INT_MIN
トラップ表現について
Rust などではアンダフローを「パニック(トラップ)」とするアプローチもあるが不十分である。
- 理由: コバレッジが不十分で、非自明なプログラムではテストしきれない。
- サービス可用性: パニックはサービス拒否攻撃に対して脆弱であり好ましくない。
- C/C++ vs Rust: C/C++ は無符号を巻戻し(安全)、有符号をトラップする。一方 Rust は逆(有符号も無符号も条件付き)となり、整数演算においてのみ有効なコードが得られる。
あなたの反論は病的入力についてである
- 「悪意のある入力か」と判断するのは時間見積もりと同等の精度しかない。要件は常に変化する。
- 過去の事例(例:qmail の CVE)は、有符号整数演算を避け、病的入力を直接的に扱うことで防ぐことが可能であることを示す。
無符号整数のための議論
プログラム内の大多数の整数は決して負の値を表現しない
- 型による指示: 無符号整数は「負ではない量」を示すのに適した型である(サイズ付き整数型のよう)。
- 統計的根拠: Intel の研究によると、デスクトップ x86 Windows システム全体の整数のうちわずか 3% だけが負の値を表している。
- その場合でも、コードベースの約97% は有符号であるべきではない。
コンパイラ診断は無符号にとって良いが、全体としては悪
- 混在した使用でコンパイル警告が出ることがあるが、これらは助けることを意図しているが、能動的に悪意的な挙動を示す(沈黙させることで危険なキャストを許容)。
- 色の例え:
- 有符号 = 赤
- 無符号 = 青
- 例外は負の値のみ。ルールは「すべてが同じ符号性を共有する」方が適切であるため、全てを青色(無符号)にするのが最適。
オーバーフローおよびアンダフローをチェックするのが容易で安全である
- 有符号の問題: C/C++ では未定義行動となるため、チェックコード(
など)はコンパイルエラーに繋がる。if (x + y < x) - 無符号の解決策: 完全に安全かつ適切。
- 例外:Go/Rust/Odin の有符号巻戻し定義では
およびINT_MIN
で問題が発生する可能性がある(LLVM の限界)。INT_MAX - 冗長なチェック例(覚える必要のない怪物コード):
if ((b > 0 && a > INT_MAX - b) || (b < 0 && a < INT_MIN - b)) - 例外:Go/Rust/Odin の有符号巻戻し定義では
コードは単純で速くなる
- 境界チェックの削減: 負になることがないため、
チェック(有符号の NULL ポインタに相当)のようなテストが不要になる。NULL - 最適化の利点: コンパイラはオーバーフローがないことを仮定してより高度な最適化を行うことができる。
- 値範囲分析:
はb = a + 2
を確立し、コンパイラに有効な手がかりを与える。b > a
- 値範囲分析:
それは実際に機能します
- 有符号整数を避けるとコードが容易でクリーンになり、強固になる。
- 注意: 既存のコードベースへ導入することはお勧めしない(静かなバグの原因となる可能性がある)。
- 推奨: 新しいプロジェクトではまず無符号整数を使用することを試してみる。