web-dev-qa-db-ja.com

符号固有またはサイズ固有の型に対して「int」を使用する必要があるのはいつですか?

私は little VM をCで実装しています。32ビットと64ビットの両方のアーキテクチャ、およびCとC++の両方でのコンパイルをサポートしています。

できるだけ多くの警告を有効にして、問題なくコンパイルできるようにしています。 CLANG_WARN_IMPLICIT_SIGN_CONVERSIONをオンにすると、一連の新しい警告が表示されます。

intを明示的に署名されていない型や明示的にサイズが指定されている型と比較して、いつ使用するかについての適切な戦略が欲しいのですが。これまでのところ、私はその戦略がどうあるべきかを決めるのに苦労しています。

それらを混合すること(ローカル変数やパラメーターなどの場合は主にintを使用し、構造体のフィールドにはより狭い型を使用すること)が多くの暗黙の変換問題を引き起こすことは確かに事実です。

ヒープ内のオブジェクトのメモリ使用量を明示的に制御するアイデアが好きなので、構造体フィールドにはより具体的なサイズのタイプを使用するのが好きです。また、ハッシュテーブルでは、ハッシュ時に符号なしオーバーフローに依存しているため、ハッシュテーブルのサイズがuint32_tとして保存されていると便利です。

しかし、もっと具体的なタイプeverywhereを使おうとすると、どこにでも曲がりくねったキャストの迷路の中にいます。

他のCプロジェクトは何をしますか?

56
munificent

キャストの必要性を最小限に抑えるため、どこでもintを使用するだけで魅力的なように思えるかもしれませんが、いくつかの潜在的な落とし穴があります。

  • intは予想よりも短い場合があります。ほとんどのデスクトッププラットフォームでは、intは通常32ビットですが、 C標準では最小長は16ビットしか保証されません 。 2より大きい数値が必要16−1 = 32,767、一時的な値でも?その場合は、intを使用しないでください。 (代わりにlongを使用することもできます。longは少なくとも32ビットであることが保証されています。)

  • longでも十分に長いとは限りません。特に、配列(またはchar配列である文字列)の長さがlongに収まる保証はありません。size_tまたはptrdiff_t、署名済みの差分が必要な場合)。

    特に、 size_tは、有効な配列インデックスを保持するのに十分な大きさに定義されています ですが、intまたはlongはそうではない場合があります。したがって、たとえば、配列を反復処理する場合、少なくとも配列が短い型で機能するために十分に短いことが確実でない限り、ループカウンター(およびその初期値/最終値)は通常size_tでなければなりません。 (ただし、逆方向に反復する場合は注意してください:size_tは符号なしなので、for(size_t i = n-1; i >= 0; i--)は無限ループです!i != SIZE_MAXまたはi != (size_t) -1を使用しても機能します。または、do/whileループを使用しますが、ケースn == 0!)

  • _(intが署名されています。特に、これは intオーバーフローが未定義の動作です。 値が合法的にオーバーフローするリスクがある場合は、intを使用せず、unsigned intまたは、代わりにunsigned long、またはuintNN_t)を使用します。

  • _(場合によっては、固定ビット長が必要なだけです。ABIとのインターフェース、またはファイル形式の読み取り/書き込みを行う場合、特定の長さの整数が必要であり、それを使用する必要があります(Ofもちろん、そのような状況では、エンディアンなどのことも心配する必要があるかもしれません。そのため、とにかく手動でデータをバイト単位でパックする必要があるかもしれません。)

そうは言っても、固定長の型を常に使用しないようにする理由もあります。int32_tが常に型を入力するのが面倒なだけでなく、コンパイラーに常に32ビット整数を使用するように強制することは、特にプラットフォームでは必ずしも最適ではありません。ここで、ネイティブのintサイズは、たとえば64ビットです。あなたはたとえばC99 int_fast32_tを使用することができますが、入力するのはさらに厄介です。


したがって、最大の安全性と移植性のための私の個人的な提案は次のとおりです。

  • カジュアルな使用のために独自の整数型を定義する次のような共通のヘッダーファイルで= /

    #include <limits.h>
    typedef int i16;
    typedef unsigned int u16;
    #if UINT_MAX >= 4294967295U
      typedef int i32;
      typedef unsigned int u32;
    #else
      typedef long i32;
      typedef unsigned long i32;
    #endif
    

    これらの型は、十分な大きさである限り、型の正確なサイズが重要ではない場合に使用します。私が提案した型名は短くて自己文書化されているため、必要に応じてキャストで簡単に使用でき、型が狭すぎることによるエラーのリスクを最小限に抑える必要があります。

    上記のように定義されたu32およびu16型は、少なくともunsigned intと同じ幅であることが保証されているため、 intにプロモートされ、未定義のオーバーフロー動作が発生することを心配せずに安全に使用できます。

  • すべての配列サイズとインデックスにsize_tを使用しますが、それと他の整数型との間でキャストするときは注意してください。必要に応じて、アンダースコアをそれほど多く入力したくない場合は、typedefを使用すると便利です。

  • 特定のビット数でのオーバーフローを想定する計算では、uintNN_tを使用するか、上記で定義したu16/u32を使用し、&で明示的なビットマスキングを行います。 uintNN_tを使用する場合は、intへの予期しない昇格から身を守るようにしてください。これを行う1つの方法は、次のようなマクロを使用することです。

    #define u(x) (0U + (x))
    

    これで安全に書けるはずです:

    uint32_t a = foo(), b = bar();
    uint32_t c = u(a) * u(b);  /* this is always unsigned multiply */
    
  • 特定の整数長を必要とする外部ABIの場合は、特定のタイプを再度定義します。例:

    typedef int32_t fooint32;  /* foo ABI needs 32-bit ints */
    

    繰り返しますが、このタイプ名は、サイズと目的の両方に関して、自己文書化されています。

    プラットフォームやコンパイル時のオプションに応じて、ABIが実際に代わりに16ビットまたは64ビットのintを必要とする可能性がある場合は、型定義を変更して一致させることができます(そして型の名前をfoointに変更します)。予期せずオーバーフローする可能性があるので、その型にキャストしたり、その型からキャストしたりするときは、常に注意する必要があります。

  • コードに特定のビット長を必要とする独自の構造またはファイル形式がある場合は、外部ABIであるかのように、それらにもカスタムタイプを定義することを検討してください。または、代わりにuintNN_tを使用することもできますが、その方法では自己文書化が少し失われます。

  • これらすべての型について、境界チェックを簡単にするために、対応する_MINおよび_MAX定数も定義することを忘れないでください。これは大変な作業のように思えるかもしれませんが、実際には1つのヘッダーファイルの数行にすぎません。

最後に、整数演算、特にオーバーフローには注意してください。たとえば、2つのnビットの符号付き整数の違いは、nビットのintに収まらない場合があることに注意してください。 (負でないことがわかっている場合は、nビットunsignedintに適合しますが、入力を符号なしの型beforeにキャストして、未定義の動作を回避するためにそれらの違いを取ります!)同様に、2つの整数の平均を見つけるには(たとえば、バイナリ検索の場合)、 avg = (lo + hi) / 2ですが、たとえばavg = lo + (hi + 0U - lo) / 2;前者は合計がオーバーフローすると壊れます。

31
Ilmari Karonen

私が一目見たリンク先のソースコードから判断すると、あなたは自分が何をしているか知っているようです。

自分で言った-「特定の」タイプを使用すると、キャストが増える。それはとにかく取るのに最適なルートではありません。より特殊な型を要求しないものについては、intを可能な限り使用してください。

intのすばらしいところは、あなたが話す型を抽象化していることです。これは、intを認識しないシステムに構成を公開する必要がないすべての場合に最適です。これは、プログラムのプラットフォームを抽象化するための独自のツールです。場合によっては、速度、サイズ、配置の利点も得られます。

他のすべての場合、例えば意図的にマシンの仕様に近づけたい場合は、intを破棄することができます。典型的なケースには、データがネットワーク上を流れるネットワークプロトコル、および相互運用機能-C言語と他の言語間の一種のブリッジ、C構造体にアクセスするカーネルアセンブリルーチンが含まれます。ただし、これらの場合でも実際にはintを使用したい場合があることを忘れないでください。これは、プラットフォームが「ネイティブ」または優先Wordのサイズに従っており、そのプロパティに依存したい場合があるためです。

uint32_tのようなプラットフォームタイプでは、Cとアセンブラの両方からアクセスされる場合、カーネルはこれらのデータ構造でこれらを使用する必要があります(必須ではない場合があります)。後者は通常intが何であるかを知らないためです。察するに。

要約すると、可能な限りintを使用し、必要に応じて、より抽象的なタイプから「マシン」タイプ(バイト/オクテット、ワードなど)に移動することをお勧めします。

size_tおよびその他の「使用法を示唆する」型について-構文がその型に固有のセマンティクスに従う限り-たとえば、size_tを適切に使用すると、サイズの値すべての種類の-私は争わないでしょう。しかし、それが最大の型であることが保証されているという理由だけで、実際にそれを自由に適用することはしません(実際にそれが真実であるかどうかに関係なく)。それは、後で踏みたくない水中の石です。コードは可能な限り自明でなければならない、と私は言うでしょう-当然のことながら何も期待されていないところにsize_tがあると、正当な理由で眉毛が上がるでしょう。サイズにはsize_tを使用します。オフセットにはoffset_tを使用します。オクテット、単語などには[u]intN_tを使用します。等々。

これは、特定のCタイプに固有のセマンティクスをソースコードに適用すること、および実行中のプログラムへの影響についてです。

また、他の人が示したように、typedefを避けないでください。これにより、独自の型、つまり私が個人的に重視している抽象化機能を効率的に定義できるようになります。優れたプログラムソースコードは、単一のintを公開することすらできませんが、それでも、多数の目的定義型の背後にあるエイリアスされたintに依存しています。ここでは、typedefについては説明しませんが、他の答えはうまくいきます。

14
amn

配列のメンバーにアクセスするために使用される多数を保持するか、size_tとしてバッファーを制御します。

size_tを使用するプロジェクトの例については、 GNUのdd.c、155行目 を参照してください。

7
none

ここでは、いくつかのことを行います。彼らが皆のためであるかどうかわかりませんが、彼らは私のために働きます。

  1. intまたはunsigned intを直接使用しないでください。ジョブには、より適切な名前のタイプが常にあるようです。
  2. 変数を特定の幅にする必要がある場合(ハードウェアレジスタの場合やプロトコルと一致させる場合など)は、幅固有の型(uint32_tなど)を使用します。
  3. 配列反復子の場合、配列要素0からnにアクセスしたいので、これも符号なし(0未満のインデックスにアクセスする理由はありません)で、高速タイプの1つ(例:uint_fast16_t)を使用して、すべての配列要素にアクセスするために必要な最小サイズに基づいた型。たとえば、最大24要素を反復するforループがある場合は、uint_fast8_tを使用して、コンパイラー(またはstdint.h、取得したい知識に応じて)に決定させますこれは、その操作で最速のタイプです。
  4. 署名される特別な理由がない限り、常に署名されていない変数を使用します。
  5. 符号なし変数と符号付き変数を一緒に再生する必要がある場合は、明示的なキャストを使用して、結果に注意してください。 (幸いにも、絶対に必要な場合を除いて、符号付き変数の使用を避ければ、これは最小限になります。)

それらのいずれかに同意しない場合、または推奨される代替案がある場合は、コメントでお知らせください!それがソフトウェア開発者の人生です...私たちは学び続けるか、無関係になります。

1
Andrew Cottrell

常に。

16ビットプラットフォームを使用しており、32767より大きい整数が必要な場合や、ネットワークまたはファイル内のデータ交換のために適切なバイトオーダーとサイネージを確保する必要がある場合など、より具体的なタイプを使用する特定の理由がない限り(リソースに制約がない場合は、データを「プレーンテキスト」で転送することを検討してください。つまり、ASCIIまたは必要に応じてUTF8)を意味します。

私の経験では、「intを使用するだけ」が生きるのに適した格言であり、作業しやすく、保守が容易で、正しいコードを毎回すばやく作成できることがわかりました。ただし、具体的な状況は異なる可能性があるため、このアドバイスを十分に精査してください。

0
Scott