web-dev-qa-db-ja.com

なぜsize_tは署名されていないのですか?

Bjarne StroustrupがC++プログラミング言語で書いた:

符号なし整数型は、ストレージをビット配列として扱う用途に最適です。 intの代わりにunsignedを使用してビットを1つ増やして正の整数を表すことは、ほとんど決して良い考えではありません。変数を符号なしで宣言することにより、一部の値が正であることを確認しようとする試みは、通常、暗黙の変換ルールによって無効になります。

size_tは、「正の整数を表すためにもう1ビットを得る」ために符号なしのようです。これは間違い(またはトレードオフ)でしたか、そうであれば、独自のコードでの使用を最小限に抑える必要がありますか?

Scott Meyersによる別の関連記事は here です。要約すると、彼は、値が常に正であるかどうかに関係なく、署名されていないインターフェイスを使用しないことをお勧めします。つまり、負の値が意味をなさなくても、必ずしも符号なしを使用する必要はありません。

59
Jon

size_tは、歴史的な理由により署名されていません。

「小さな」モデルのDOSプログラミングなど、16ビットポインタを備えたアーキテクチャでは、文字列を32 KBに制限することは現実的ではありません。

このため、C規格では、(必要な範囲を介して)ptrdiff_tsize_tに対応する符号付きの対応物、およびポインタ差分の結果の型を、実質的に17ビットにする必要があります。

これらの理由は、組み込みプログラミングの世界の一部にも当てはまります。

ただし、最新の32ビットまたは64ビットプログラミングには適用されません。より重要な考慮事項は、CおよびC++の不幸な暗黙の変換ルールにより、符号なしの型が数値に使用された場合にバグアトラクターになることです(およびしたがって、算術演算とマグニチュード比較)。 20から20の考え方で、これらの特定の変換ルールを採用する決定が行われたことがわかります。 string( "Hi" ).length() < -3は実質的に保証されており、かなりばかげて非実用的でした。ただし、その決定は、現代のプログラミングでは、数値に符号なしの型を採用すると、unsignedが自己記述的な型名であると感じ、typedef int MyTypeを考えることに失敗した人の気持ちを満たすことを除いて、重大な欠点と利点がないことを意味します。

要約すると、それは間違いではありませんでした。それは当時非常に合理的で実用的なプログラミング上の理由からの決定でした。 Pascalのような境界チェック済み言語からの期待をC++に転送することとは何の関係もありません(これは誤りですが、Pascalを聞いたことがない人でも非常に一般的なものです)。

59

size_tunsignedです。負のサイズは意味がないためです。

(コメントより:)

それが何であるかを述べているように、それはそれほど確実ではありません。サイズ-1のリストを最後に見たのはいつですか?そのロジックをあまりにも遠くに追いかけると、符号なしが存在してはならず、ビット操作も許可されるべきではないことがわかります。 – geekosaur

要点:考えなければならない理由により、住所は署名されていません。サイズは住所を比較することによって生成されます。住所を署名済みとして扱うと非常に間違った結果が生じ、結果に署名済みの値を使用するとデータが失われ、Stroustrup見積もりの​​読み取りは明らかに受け入れられると考えられますが、実際にはそうではありません。おそらく、代わりに負のアドレスが何をすべきかを説明できます。 – geekosaur

26
geekosaur

インデックスタイプを符号なしにする理由は、ハーフオープンインターバルに対するCおよびC++の設定との対称性のためです。また、インデックスタイプが符号なしになる場合は、サイズタイプも符号なしにしておくと便利です。


Cでは、配列を指すポインターを持つことができます。有効なポインタは、配列の任意の要素、または配列の末尾を過ぎた1つの要素を指すことができます。配列の開始前の1つの要素を指すことはできません。

int a[2] = { 0, 1 };
int * p = a;  // OK
++p;  // OK, points to the second element
++p;  // Still OK, but you cannot dereference this one.
++p;  // Nope, now you've gone too far.
p = a;
--p;  // oops!  not allowed

C++はこれに同意し、このアイデアをイテレータに拡張します。

符号なしのインデックスタイプに対する引数は、配列を後ろから前にトラバースする例を示していることが多く、コードは次のようになります。

// WARNING:  Possibly dangerous code.
int a[size] = ...;
for (index_type i = size - 1; i >= 0; --i) { ... }

このコードは動作しますonly if index_typeは署名されています。これは、インデックスタイプに署名する必要があるという引数として使用されます(拡張により、サイズに署名する必要があります)。

そのコードは慣用的ではないため、その議論は説得力がありません。このループをインデックスではなくポインタで書き換えようとするとどうなるか見てください。

// WARNING:  Bad code.
int a[size] = ...;
for (int * p = a + size - 1; p >= a; --p) { ... }

はい、今私たちは未定義の動作をしています! sizeが0の場合の問題を無視すると、最初の前の要素を指す無効なポインターを生成するため、反復の最後に問題があります。これは、ポインターを逆参照しようとしても、未定義の動作です。

そのため、言語標準を変更して、最初の要素の前に要素を指すポインターがあることを合法にすることでこれを修正することを主張できますが、それは起こりそうにありません。ハーフオープン間隔はこれらの言語の基本的な構成要素なので、代わりに、より優れたコードを記述しましょう。

正しいポインタベースのソリューションは次のとおりです。

int a[size] = ...;
for (int * p = a + size; p != a; ) {
  --p;
  ...
}

多くの人は、デクリメントがヘッダーではなくループの本体にあるため、これが邪魔になることに気づきますが、for-syntaxが主にハーフオープンインターバルのフォワードループ用に設計されている場合に起こります。 (逆反復子は、デクリメントを延期することによってこの非対称性を解決します。)

類推すると、インデックスベースのソリューションは次のようになります。

int a[size] = ...;
for (index_type i = size; i != 0; ) {
  --i;
  ...
}

これはindex_typeは符号付きまたは符号なしですが、符号なしを選択すると、慣用的なポインターおよびイテレーターのバージョンにより直接的にマップするコードが生成されます。符号なしは、ポインタやイテレータと同様に、シーケンスのすべての要素にアクセスできることを意味します。意味のない値を表すために、可能な範囲の半分を引き渡さないことを意味します。これは64ビットの世界では実用的な問題ではありませんが、16ビットの組み込みプロセッサや、同じAPIを引き続き提供できる大規模な範囲にわたるスパースデータの抽象コンテナータイプの構築では、非常に現実的な問題になる可能性があります。ネイティブコンテナ。

3
Adrian McCarthy

一方 ...

神話1std::size_tが署名されていません。これは、もはや適用されないレガシー制限のためです。

ここで一般的に参照される「歴史的な」理由は2つあります。

  1. sizeofは、Cの時代から署名されていないstd::size_tを返します。
  2. プロセッサーはワードサイズが小さいため、その範囲の余分なビットを絞ることが重要でした。

しかし、これらの理由のどちらも、非常に古いにもかかわらず、実際には歴史に追いやられています。

sizeofは、まだ署名されていないstd::size_tを返します。 sizeofまたは標準ライブラリコンテナと相互運用する場合は、std::size_tを使用する必要があります。

代替手段はすべてさらに悪い:符号付き/符号なしの比較警告とサイズ変換警告を無効にし、値が常に重複する範囲にあることを期待して、異なるタイプのカップルを使用して潜在的なバグを無視できるようにすることができます。または、範囲チェックと明示的な変換のlotを実行することもできます。または、範囲チェックを一元化するための巧妙な組み込み変換を使用して独自のサイズタイプを導入することもできますが、他のライブラリはあなたのサイズタイプを使用しません。

また、ほとんどの主流のコンピューティングは32ビットと64ビットのプロセッサーで行われますが、C++は今日でも組み込みシステムの16ビットマイクロプロセッサーで使用されています。これらのマイクロプロセッサでは、メモリスペース内の任意の値を表すことができるワードサイズの値があると非常に便利です。

新しいコードは、標準ライブラリと相互運用する必要があります。標準ライブラリが引き続き符号なしの型を使用している間に、新しいコードが符号付きの型を使用した場合、両方を使用する必要のあるすべてのコンシューマーにとって困難になります。

神話2:その余分なビットは必要ありません。 (別名、アドレススペースが4 GBしかない場合、2 GBを超える文字列はありません。)

サイズとインデックスはメモリだけではありません。アドレス空間が制限されている可能性がありますが、アドレス空間よりはるかに大きいファイルを処理する場合があります。また、2GB以上の文字列がない場合でも、2Gビット以上のビットセットを快適に使用できます。まばらなデータ用に設計された仮想コンテナを忘れないでください。

神話:常により広い符号付きタイプを使用できます。

常にではない。 1つまたは2つのローカル変数の場合、std::int64_t(システムに1つあると想定)またはsigned long longを使用して、おそらく完全に妥当なコードを記述できることは事実です。 (ただし、いくつかの明示的なキャストと2倍の境界チェックが必要になるか、コードの他の場所にバグがあることを警告するコンパイラ警告を無効にする必要があります。)

しかし、インデックスの大きなテーブルを作成している場合はどうでしょうか。 1つだけ必要なbitのときに、すべてのインデックスに追加の2つまたは4つのbytesが本当に必要ですか?十分なメモリと最新のプロセッサがある場合でも、そのテーブルを2倍の大きさにすると、参照の局所性に悪影響を与える可能性があり、すべての範囲チェックが2段階になり、分岐予測の効果が低下します。そして、もしあなたがそのすべての記憶を持っていない場合はどうなりますか?

神話4:符号なし演算は意外で不自然です。

これは、signed演算が驚くべきことではなく、どういうわけかより自然であることを意味します。そして、それはおそらく、数学の観点から考えると、すべての基本的な算術演算がすべての整数のセットに対して閉じられている場合です。

しかし、私たちのコンピューターは整数では動作しません。それらは整数のごくわずかな部分で動作します。私たちの符号付き算術は、すべての整数のセットに対して閉じられていません。オーバーフローとアンダーフローがあります。多くの人にとって、それは驚くべき不自然なことであり、彼らはほとんどそれを無視しています。

これはバグです:

auto mid = (min + max) / 2;  // BUGGY

minmaxが署名されている場合、合計がオーバーフローする可能性があり、未定義の動作が発生します。私たちのほとんどは、この種のバグを日常的に見逃しています。これは、署名された整数のセットに対して追加が閉じられていないことを忘れているためです。私たちのコンパイラーは通常、妥当な(しかしまだ驚くべき)ことを行うコードを生成するので、それを回避します。

minおよびmaxが符号なしの場合、合計はオーバーフローする可能性がありますが、未定義の動作はなくなります。あなたはまだ間違った答えを得るでしょう、それでそれはまだ驚くべきことです、しかしそれはsigned intであったよりも驚くべきことではありません。

署名されていない本当の驚きには、引き算が伴います。小さい署名された整数から大きい署名されていないintを引き算すると、大きな数になります。この結果は、0で除算した場合よりも驚くべきことではありません。

すべてのAPIから未署名の型を排除できたとしても、標準のコンテナー、ファイル形式、またはワイヤープロトコルを扱う場合は、これらの未署名の "驚き"に備える必要があります。問題の一部のみを「解決」するために、APIに摩擦を追加する価値は本当にありますか?

1
Adrian McCarthy