1. ホーム
  2. c++

[解決済み] 符号付きintではなく符号なしintを使用するとバグが発生しやすいですか?なぜですか?

2023-06-28 06:24:24

質問

この質問では Google C++ スタイルガイド では、「符号なし整数」のトピックで、次のように提案されています。

歴史的な偶然から、C++ 標準はコンテナのサイズを表すために符号なし整数も使用しています。標準化団体の多くのメンバーはこれを間違いであると考えていますが、現時点では事実上修正することは不可能です。符号なし演算は単純な整数の動作をモデル化しておらず、代わりにモジュール演算 (オーバーフロー/アンダーフローで折り返す) をモデル化するように標準によって定義されているという事実は、バグのかなりのクラスがコンパイラーによって診断されないということを意味します。

モジュール式演算の何が問題なのでしょうか?それは符号なしintの期待される動作ではないでしょうか?

ガイドはどのようなバグ(重要なクラス)を指しているのでしょうか?オーバーフローするバグですか?

変数が非負であることを主張するためだけにunsigned型を使用しないでください。

unsigned intよりもsigned intを使う理由として考えられるのは、(負に)オーバーフローする場合、それを検出するのがより簡単であるということです。

どのように解決するのですか?

ここの回答の中には、符号付き値と符号なし値の間の驚くべき昇格ルールに言及しているものがありますが、これはどちらかというと、以下の問題に関連しているように思われます。 ミキシング を説明するものではありません。 符号あり 変数が 符号なし の方が望ましいでしょう。

私の経験では、混合比較と昇格ルールの外で、符号なし値がバグの磁石である2つの主な理由が以下のようにあります。

符号なし値は、プログラミングで最も一般的な値であるゼロで不連続になります。

符号なし整数と符号付き整数の両方には 不連続性 があり、そこで折り返したり(符号なし)、不定な振る舞いをします(符号あり)。については unsigned の場合、これらの点は ゼロ UINT_MAX . については int にあります。 INT_MIN であり INT_MAX . の典型的な値は INT_MININT_MAX を持つシステムで、4バイトの int の値は -2^31 であり 2^31-1 であり、そのようなシステム上では UINT_MAX は通常 2^32-1 .

の主なバグを誘発する問題は unsigned には適用されませんが int があることです。 ゼロでの不連続性 . ゼロはもちろん、1,2,3のような小さな値とともに、プログラムでは非常によく使われる値です。様々な構成で小さな値、特に 1 を足したり引いたりするのはよくあることで、もしあなたが unsigned 値から何かを引いて、それがたまたまゼロであった場合、巨大な正の値を得てしまい、ほぼ確実にバグが発生します。

ベクトル内のすべての値をインデックスで反復するコードを考えてみましょう。 0.5 :

for (size_t i = 0; i < v.size() - 1; i++) { // do something }

これはある日、空のベクトルを渡すまではうまくいきます。ゼロ反復ではなく、次のようになります。 v.size() - 1 == a giant number 1 というように、40億回の繰り返しをして、ほぼバッファオーバーフローの脆弱性を持つことになります。

こんな風に書かないといけない。

for (size_t i = 0; i + 1 < v.size(); i++) { // do something }

というわけで、この場合は "fixed"することができます。 size_t . 定数ではなく、適用したい可変オフセットがあり、それが正または負であるため、上記の修正が適用できない場合があります。 本当に となります。

ゼロまで、そしてゼロを含めて繰り返し処理しようとするコードにも、同様の問題があります。次のようなものです。 while (index-- > 0) は問題なく動作しますが、明らかに同等の while (--index >= 0) は符号なし値では決して終了しません。コンパイラは、右辺が リテラル ゼロである場合、コンパイラは警告を出すかもしれませんが、実行時に決定される値である場合は確実に警告を出しません。

カウンターポイント

符号付き値にも 2 つの不連続性があり、なぜ符号なしを選ぶのか、と主張する人がいるかもしれません。違いは、両方の不連続点がゼロから非常に (最大に) 遠いということです。私は、これは、符号付き値と符号なし値の両方が非常に大きな値でオーバーフローする可能性がある、quot; overflow" の別の問題であると本当に考えています。多くの場合、値の取り得る範囲に制約があるため、オーバーフローは不可能であり、多くの64ビット値のオーバーフローは物理的に不可能です)。たとえ可能であっても、オーバーフローに関連するバグの可能性は、"at zero" バグと比較して非常に小さいことが多く、また オーバーフローは符号なし値でも発生します。 . つまり、符号なしは、非常に大きな値のオーバーフローの可能性と、ゼロでの不連続性という、両方の悪い面を兼ね備えています。符号付きは前者のみです。

多くの人が、符号なしはビットを失うと主張するでしょう。多くの場合、これは真実ですが、常にそうとは限りません (符号なしの値間の相違を表現する必要がある場合、いずれにせよそのビットを失うことになります: 非常に多くの 32 ビット物はいずれにせよ 2 GiB に制限されており、また、たとえばファイルが 4 GiB であっても、2 GiB 半分の特定の API を使用できないような奇妙なグレーゾーンが存在することになります)。

もしあなたが 20 億以上の "物事" をサポートしなければならなかったとしたら、おそらくすぐに 40 億以上をサポートしなければならないでしょう。

論理的には、符号なし値は符号付き値のサブセットである

数学的には、符号なし値(非負整数)は符号付き整数(単に_integerと呼ばれます)のサブセットです。 2 . しかし は署名されています。 のみの操作では、自然に値が飛び出します。 符号なし に対する演算、例えば引き算のようなものです。私たちは、符号なし値は 閉じた は引き算の下では閉じない。同じことが符号付き値には当てはまりません。

ファイルへの2つの符号なしインデックス間の "delta"を見つけたいですか?正しい順序で減算を行う必要があり、そうでなければ間違った答えを得ることになります。もちろん、正しい順序を決定するために実行時のチェックが必要なこともよくあります。数値として符号なしの値を扱う場合、(論理的に)符号付きの値が常に現れることがよくあるので、符号付きから始めた方がよいでしょう。

カウンターポイント

上記の脚注(2)で述べたように、C++の符号付き値は実は同じ大きさの符号なし値のサブセットではないので、符号なし値も符号付き値と同じ数の結果を表すことができます。

その通りですが、範囲はあまり有用ではありません。引き算を考え、0 から 2N の範囲を持つ符号なし数、および -N から N の範囲を持つ符号付き数。任意の引き算は、両方のケースで -2N から 2N の範囲の結果になり、どちらのタイプの整数もその半分しか表すことができません。しかし、0から2Nの範囲よりも、-NからNの0を中心とした範囲の方が、より有用である(実際のコードでより多くの結果を含んでいる)ことがわかります。一様以外の任意の典型的な分布 (log、zipfian、normal、その他) を考え、その分布からランダムに選択した値を差し引くことを検討してください: [0, 2N] より [-N, N] で終わる値の方がはるかに多くなります (実際、結果の分布は常にゼロで中心が決まっています)。

64 ビットは、数値として符号なし値を使用する理由の多くにドアを閉じます。

私は、上記の議論は 32 ビット値に対してすでに説得力があったと思いますが、オーバーフローのケースは、異なる閾値で符号付きと符号なしの両方に影響を及ぼします。

する

は、多くの抽象的および物理的な量 (数十億ドル、数十億ナノ秒、数十億個の要素を持つ配列) によって超えられる数であるため、32 ビット値に対して発生します。したがって、誰かが符号なし値の正の範囲の倍増によって十分に納得すれば、オーバーフローは重要であり、符号なしがわずかに有利であると主張することができます。

特殊な領域以外では、64ビット値はこの懸念をほとんど取り除きます。符号付き 64 ビット値の上限範囲は 9,223,372,036,854,775,807 です。 キンチョー . これは大量のナノ秒 (約 292 年分) であり、大量のお金です。また、どのコンピューターも長い間、コヒーレントなアドレス空間に RAM を持つ可能性があるため、それよりも大きな配列となります。ですから、9,000,000,000個あれば、誰にとっても(今のところ)十分なのではないでしょうか?

符号なし値を使用する場合

スタイルガイドでは、符号なしの数値の使用を禁じているわけでも、必ずしも推奨しているわけでもないことに注意してください。それは次のように結ばれています。

単に変数が非負であることを主張するために符号なし型を使用しないでください。

実際、符号なし変数には良い使い方があります。

  • Nビットの量を整数としてではなく、単に "ビットの袋" として扱いたいとき。例えば、ビットマスクやビットマップ、N個のブール値などとして。この用途はしばしば、以下のような固定幅の型と密接に関係します。 uint32_tuint64_t というように、変数の正確な大きさを知りたいことがよくあるからです。特定の変数がこの扱いに値するというヒントは、その変数に対して ビット単位 のような演算子で操作することです。 ~ , | , & , ^ , >> などの算術演算ではありません。 + , - , * , / など。

    ビット演算子の動作がよく定義され、標準化されているため、ここでは符号なしが理想的です。符号付き値には、シフト時の動作が未定義であることや、表現が不特定であることなど、いくつかの問題があります。

  • 実際にモジュラー演算が必要な場合。実際に2^Nのモジュラー演算が必要な場合もあります。このような場合、"overflow" は機能であり、バグではありません。符号なし値は、モジュール演算を使うように定義されているので、ここで欲しいものを与えてくれます。符号付き値は、不特定の表現を持ち、オーバーフローが未定義であるため、(簡単に、効率的に)まったく使用することができません。


0.5 これを書いた後、これはほぼ同じであることに気づきました。 Jarod の例 とほぼ同じであることに気づきましたが、私はこれを見たことがありませんでした。

1 私たちが話しているのは size_t で、通常 32 ビットシステムでは 2^32-1、64 ビットシステムでは 2^64-1 です。

2 C++では、符号なし値は対応する符号付き型よりも上位に多くの値を含むため、これは正確なケースではありませんが、基本的な問題は、符号なし値を操作すると(論理的に)符号付き値になることがありますが、符号付き値では(すでに符号なし値を含むため)対応する問題が存在しません。