web-dev-qa-db-ja.com

署名された整数ではなく符号なしの整数を使用すると、バグが発生する可能性が高くなりますか?どうして?

Google C++ Style Guide の「符号なし整数」のトピックでは、

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

モジュラー演算の何が問題になっていますか?それは、unsigned intの予想される動作ではありませんか?

このガイドではどのようなバグ(重要なクラス)を参照していますか?あふれるバグ?

変数が負でないことをアサートするためだけに、符号なしの型を使用しないでください。

符号なし整数を符号なし整数よりも使用することを考えられる理由の1つは、オーバーフローした場合(負の値に)検出するのが簡単だからです。

76
user7586189

ここでの回答のいくつかは、符号付きの値と符号なしの値の間の驚くべき昇格規則に言及していますが、これは混合符号付きと符号なしの値に関連する問題のようであり、必ずしも符号付きが優先される理由を説明していませんnsigned、ミキシングシナリオ外。

私の経験では、混合比較とプロモーションルール以外に、符号なしの値が大きなバグの原因となる2つの主な理由があります。

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

符号なし整数と符号付き整数の両方には、最小値と最大値に不連続性があり、ラップアラウンド(符号なし)または未定義の動作(符号付き)を引き起こします。 unsignedname__の場合、これらのポイントはzeroおよびUINT_MAXにあります。 intname__の場合、それらはINT_MINおよびINT_MAXにあります。 4バイトのintname__値を持つシステムでのINT_MINおよびINT_MAXの一般的な値は-2^31および2^31-1であり、そのようなシステムではUINT_MAXは通常2^32-1です。

unsignedname__に適用されないintname__のバグを引き起こす主な問題は、不連続点がゼロを持つことです。もちろん、ゼロはプログラムで非常に一般的な値であり、1,2,3のような他の小さな値も同様です。さまざまなコンストラクトで小さな値、特に1を加算および減算するのが一般的であり、unsignedname__値から何かを減算してゼロになった場合、大きな正の値とほぼ確実なバグが得られます。

最後の例外を除いて、インデックスでベクトル内のすべての値を反復処理するコードを検討する0.5

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

これは、空のベクターを1日渡すまで正常に機能します。ゼロ反復を行う代わりに、v.size() - 1 == a giant numberを取得します1 また、40億回の反復を実行すると、バッファオーバーフローの脆弱性がほぼ発生します。

次のように書く必要があります。

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

したがって、この場合は「修正」できますが、size_tの符号なしの性質について慎重に考えることによってのみ可能です。上記の修正を適用できない場合があります。定数の代わりに、適用する変数オフセットがあります。これは正または負の場合があります。そのため、比較のどちらの「サイド」を署名する必要があるかによって異なります-今、コードは本当に乱雑になります。

ゼロまで反復しようとするコードにも同様の問題があります。 while (index-- > 0)のようなものは正常に機能しますが、明らかに同等のwhile (--index >= 0)は、符号なしの値では終了しません。コンパイラーは、右側がリテラルゼロの場合に警告する場合がありますが、実行時に決定された値の場合は警告しません。

カウンターポイント

署名された値には2つの不連続点があると主張する人もいるかもしれませんが、なぜ符号なしを選ぶのでしょうか?違いは、両方の不連続がゼロから非常に(最大)遠く離れていることです。私はこれを「オーバーフロー」の別の問題と本当に考えています。非常に大きな値では、符号付き値と符号なし値の両方がオーバーフローする可能性があります。多くの場合、オーバーフローは値の可能な範囲の制約のため不可能であり、多くの64ビット値のオーバーフローは物理的に不可能な場合があります)。可能な場合でも、オーバーフローに関連するバグの可能性は、「ゼロ」バグと比較して非常に小さいことが多く、符号なしの値でもオーバーフローが発生します。したがって、符号なしは、両方の世界の最悪の組み合わせです。非常に大きなマグニチュード値でオーバーフローする可能性と、ゼロでの不連続性。署名されているのは前者のみです。

多くは、「あなたは少し負けている」と無署名で主張します。これはしばしば真実です-しかし、常にではありません(符号なしの値の違いを表す必要がある場合は、とにかくそのビットを失います:多くの32ビットのものは、とにかく2 GiB_ファイルが4 GiBになる可能性のある奇妙な灰色の領域ですが、2番目の2 GiB半分では特定のAPIを使用できません。

署名なしで少し買う場合でも、あまり買わない:20億以上の「モノ」をサポートしなければならなかったら、すぐに40億以上をサポートする必要があるでしょう。

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

数学的には、符号なしの値(負でない整数)は、符号付き整数のサブセット(_integersと呼ばれる)です。2。しかし、signed値は、減算などのnsigned値のみの操作から自然にポップします。符号なしの値はclosed減算ではないと言うかもしれません。同じことは、符号付きの値には当てはまりません。

ファイルへの2つの符号なしインデックス間の「デルタ」を見つけたいですか?さて、正しい順序で減算を行うと、間違った答えが得られます。もちろん、正しい順序を決定するにはランタイムチェックが必要になることがよくあります。符号なしの値を数値として扱う場合、(論理的に)符号付きの値がとにかく表示され続けることがよくあるので、符号付きで始めることもできます。

カウンターポイント

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

本当ですが、範囲はあまり役に立ちません。減算、0〜2Nの範囲の符号なし数値、および-N〜Nの範囲の符号付き数値を検討してください。_bothの場合、任意の減算の結果は-2N〜2Nの範囲になります。その半分。 -NからNのゼロを中心とした領域は、通常、0から2Nの範囲よりもはるかに便利です(実際のコードでより実際の結果が含まれています)。一様(log、zipfian、normalなど)以外の一般的な分布を考慮し、その分布からランダムに選択した値を減算することを検討してください:[0、2N](実際には、結果の分布よりも多くの値が[-N、N]常にゼロを中心とします)。

64ビットは、符号付きの値を数値として使用する多くの理由でドアを閉めます

上記の引数は既に32ビット値に対して説得力があると思いますが、異なるしきい値で符号付きと符号なしの両方に影響するオーバーフローの場合、doは32ビット値に対して発生します。多くの抽象的および物理的な量(数十億ドル、数十億ナノ秒、数十億の要素を持つ配列)を超えることができます。したがって、誰かが符号なしの値の正の範囲を2倍にすることで十分に確信している場合、オーバーフローが重要であり、符号なしの方がわずかに有利であるというケースを作ることができます。

特化されたドメイン以外では、64ビット値はこの懸念をほとんど取り除きます。符号付き64ビット値の上限は9,223,372,036,854,775,807-9を超えるquintillionです。それは非常に多くのナノ秒(約292年の価値)であり、多くのお金です。また、コヒーレントなアドレス空間に長い間RAMを持つ可能性のあるコンピューターよりも大きいアレイです。だから、たぶん9クインティオンで十分です(今のところ)?

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

スタイルガイドは、符号なしの数値の使用を禁止したり、必ずしも推奨していません。最後に:

変数が負でないことをアサートするためだけに、符号なしの型を使用しないでください。

確かに、符号なし変数には良い使用法があります:

  • Nビット量を整数としてではなく、単に「ビットバッグ」として扱いたい場合。たとえば、ビットマスクまたはビットマップ、またはN個のブール値など。多くの場合、変数の正確なサイズを知りたいため、この使用はuint32_tuint64_tなどの固定幅タイプと密接に関係しています。特定の変数がこの処理に値するというヒントは、~|&^>>などのbitwise演算子でのみ操作し、+などの算術演算ではなく、 -*/など.

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

  • 実際にモジュラー演算が必要な場合。時々、実際には2 ^ Nモジュラー算術が必要です。これらの場合、「オーバーフロー」は機能であり、バグではありません。符号なしの値は、モジュラー演算を使用するように定義されているため、ここで必要なものを提供します。符号付きの値は表現が指定されておらず、オーバーフローが定義されていないため、(簡単に、効率的に)まったく使用できません。

0.5 これを書いた後、これは Jarodの例 とほとんど同じであることに気付きました。

1 ここではsize_tについて説明しているため、通常は32ビットシステムでは2 ^ 32-1、64ビットシステムでは2 ^ 64-1です。

2 C++では、符号なしの値は対応する符号付きの型よりも多くの値を上端に含むため、これは正確には当てはまりませんが、符号なしの値を操作すると(論理的に)符号付きの値になる可能性があるという基本的な問題がありますが、対応する問題はありません符号付きの値(符号付きの値には既に符号なしの値が含まれているため)。

66
BeeOnRope

前述のように、unsignedsignedを混在させると、(適切に定義されていても)予期しない動作が発生する可能性があります。

最後の5つを除くベクトルのすべての要素を反復処理するとします。間違って次のように記述します。

for (int i = 0; i < v.size() - 5; ++i) { foo(v[i]); } // Incorrect
// for (int i = 0; i + 5 < v.size(); ++i) { foo(v[i]); } // Correct

v.size() < 5は、v.size()unsignedであるため、s.size() - 5は非常に大きな数であり、trueのより期待される値の範囲ではi < v.size() - 5iであるとします。そして、UBはすぐに発生します(一度i >= v.size()にバインドされたアクセスから)

v.size()が符号付きの値を返す場合、s.size() - 5は負になり、上記の場合、条件はすぐにfalseになります。

一方、インデックスは[0; v.size()[の間にある必要があるため、unsignedが意味をなします。 Signedには、負の符号付き数値の右シフトに対するオーバーフローまたは実装定義の動作を伴うUBとしての独自の問題もありますが、反復のバグの発生頻度は少なくなります。

33
Jarod42

エラーの最も発生しやすい例の1つは、署名された値と署名されていない値をMIXする場合です。

#include <iostream>
int main()  {
    auto qualifier = -1 < 1u ? "makes" : "does not make";
    std::cout << "The world " << qualifier << " sense" << std::endl;
}

出力:

世界は意味をなさない

些細なアプリケーションがない限り、符号付きの値と符号なしの値の危険な組み合わせ(実行時エラーが発生する)になるか、警告を上げてコンパイル時エラーにすると、多くのエラーが発生することは避けられませんコード内のstatic_casts。そのため、数学または論理比較の型には符号付き整数を厳密に使用するのが最善です。ビットマスクとビットを表す型には符号なしのみを使用します。

数値の値の予想されるドメインに基づいて、符号なしの型をモデル化するのは悪い考えです。ほとんどの数値は20億よりも0に近いため、符号なしの型では、多くの値が有効範囲のエッジに近くなります。さらに悪いことに、finalの値は既知の正の範囲にある場合がありますが、式の評価中に中間値がアンダーフローし、中間形式で使用される場合は非常に間違った値になる可能性があります。最後に、あなたの値が常に正であると期待されていても、それはothercanが負の変数と相互作用しないという意味ではありません。署名されたタイプと署名されていないタイプを混合するという強制的な状況に対応することは、最悪の場所です。

19
Chris Uzdavinis

符号なし整数を使用するよりも、符号付き整数を使用する方がバグを引き起こす可能性が高いのはなぜですか?

nsignedタイプを使用しても、特定のタスクのクラスでsignedタイプを使用するよりもバグを引き起こす可能性は高くありません。

ジョブに適切なツールを使用します。

モジュラー演算の何が問題になっていますか?それは、unsigned intの予想される動作ではありませんか?
なぜ符号なし整数を使用するよりも、符号付き整数を使用するよりもバグを引き起こす可能性が高いのですか?

タスクがよく一致する場合:何も悪いことはありません。いいえ、そうではありません。

セキュリティ、暗号化、および認証アルゴリズムは、符号なしのモジュラー計算に依存します。

nsigned mathを使用すると、さまざまなグラフィック形式だけでなく、圧縮/解凍アルゴリズムにもメリットがあり、バグが少なくなります。

ビット単位の演算子とシフトを使用すると、nsigned演算は、signed mathの符号拡張の問題で混乱しなくなります。


符号付き整数演算は、コーディングを学習する人を含むすべての人がすぐに理解できる直感的なルックアンドフィールを備えています。 C/C++は当初は対象外でしたが、現在はイントロ言語であるべきではありません。オーバーフローに関するセーフティネットを使用する高速コーディングには、他の言語の方が適しています。無駄のない高速コードの場合、Cはコーダーが自分が何をしているかを知っている(経験がある)と想定しています。

signed mathの落とし穴は、ユビキタスな32ビットintであり、範囲チェックなしの一般的なタスクには非常に多くの問題があります。これにより、オーバーフローがコーディングされないという自己満足につながります。代わりに、nが<INT_MAXであると想定され、文字列が長すぎることはなく、最初のケースでは全範囲保護されず、2番目ではsize_tunsigned、さらにはlong longを使用するのではなく、for (int i=0; i < n; i++)int len = strlen(s);はOKと見なされます。

16ビットと32ビットのintを含む時代に開発されたC/C++と、符号なし16ビットのsize_tが提供する余分なビットは重要でした。 intまたはunsignedのオーバーフローの問題に関して注意が必要でした。

16ビット以外のint/unsignedプラットフォームでのGoogleの32ビット(またはそれ以上の)アプリケーションでは、十分な範囲があるため、intの+/-オーバーフローに注意を払えません。これは、そのようなアプリケーションがintよりもunsignedを奨励するのに意味があります。しかし、int mathは十分に保護されていません。

狭い16ビットのint/unsignedの懸念は、一部の組み込みアプリケーションに今日適用されています。

Googleのガイドラインは、今日作成するコードによく当てはまります。これは、C/C++コードのより広い範囲の範囲に対する決定的なガイドラインではありません。


符号なし整数を符号なし整数よりも使用することを考えられる理由の1つは、オーバーフローした場合(負の値に)検出するのが簡単だからです。

C/C++では、signed int math overflowはndefined behaviorであるため、nsigned mathの定義済みの動作よりも確実に検出するのは簡単ではありません。


@ Chris Uzdavinis よくコメントされているように、signednsignedの混合は、すべて(特に初心者)が避けるのが最善で、必要な場合は慎重にコーディングします。

11
chux

私は、Googleのスタイルガイドである「ヒッチハイカーガイド」を使った経験があります。この特定のガイドラインは、その本の多数のナッツルールのほんの一例です。

エラーが発生するのは、符号なしの型で算術を実行しようとした場合(上記のChris Uzdavinisの例を参照)、つまり、それらを数値として使用した場合のみです。符号なしの型は、数値を格納するために使用することを目的としておらず、負の値になることはないコンテナのサイズなど、countsを格納することを目的としています。

コンテナーのサイズを格納するために算術型(符号付き整数など)を使用するという考えはばかげています。リストのサイズを保存するためにdoubleも使用しますか? Googleに算術型を使用してコンテナサイズを保存し、他の人に同じことを要求する人がいるということは、会社について何かを物語っています。そのような指示について私が気づくのは、彼らがより愚かであるほど、彼らがより厳しい日曜大工またはあなたが解雇された規則である必要があるということです。さもないと、常識を持つ人々は規則を無視します。

5
Tyler Durden

符号なしの型を使用して非負の値を表す...

  • 他の回答が詳細に示し説明しているように、符号付きおよび符号なしの値を使用すると、型昇格に関連するバグを引き起こす可能性が高いが、
  • あまり期待されないは、望ましくない/許可されない値を表すことができるドメインを持つタイプの選択に関連するバグを引き起こすいくつかの場所では、値がドメイン内にあると想定し、他の値が何らかの方法でこっそり入ったときに予期しない、潜在的に危険な動作を起こす可能性があります。

Googleコーディングガイドラインでは、最初の考慮事項に重点を置いています。 C++ Core Guidelines などのその他のガイドラインセットでは、2番目のポイントに重点を置いています。たとえば、コアガイドライン I.12 を検討してください。

I.12:not_nullとしてnullであってはならないポインターを宣言する

理由

Nullptrエラーの逆参照を回避するため。 nullptrの冗長チェックを回避することにより、パフォーマンスを改善します。

int length(const char* p);            // it is not clear whether length(nullptr) is valid
length(nullptr);                      // OK?
int length(not_null<const char*> p);  // better: we can assume that p cannot be nullptr
int length(const char* p);            // we must assume that p can be nullptr

ソースの意図を述べることにより、実装者とツールは、静的分析を通じていくつかのクラスのエラーを見つけるなどのより良い診断を提供し、ブランチやnullテストの削除などの最適化を実行できます。

もちろん、整数のnon_negativeラッパーについて議論することもできます。これにより、両方のカテゴリのエラーを回避できますが、独自の問題が発生します...

1
einpoklum