私は little VM をCで実装しています。32ビットと64ビットの両方のアーキテクチャ、およびCとC++の両方でのコンパイルをサポートしています。
できるだけ多くの警告を有効にして、問題なくコンパイルできるようにしています。 CLANG_WARN_IMPLICIT_SIGN_CONVERSION
をオンにすると、一連の新しい警告が表示されます。
int
を明示的に署名されていない型や明示的にサイズが指定されている型と比較して、いつ使用するかについての適切な戦略が欲しいのですが。これまでのところ、私はその戦略がどうあるべきかを決めるのに苦労しています。
それらを混合すること(ローカル変数やパラメーターなどの場合は主にint
を使用し、構造体のフィールドにはより狭い型を使用すること)が多くの暗黙の変換問題を引き起こすことは確かに事実です。
ヒープ内のオブジェクトのメモリ使用量を明示的に制御するアイデアが好きなので、構造体フィールドにはより具体的なサイズのタイプを使用するのが好きです。また、ハッシュテーブルでは、ハッシュ時に符号なしオーバーフローに依存しているため、ハッシュテーブルのサイズがuint32_t
として保存されていると便利です。
しかし、もっと具体的なタイプeverywhereを使おうとすると、どこにでも曲がりくねったキャストの迷路の中にいます。
他のCプロジェクトは何をしますか?
キャストの必要性を最小限に抑えるため、どこでも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
;前者は合計がオーバーフローすると壊れます。
私が一目見たリンク先のソースコードから判断すると、あなたは自分が何をしているか知っているようです。
自分で言った-「特定の」タイプを使用すると、キャストが増える。それはとにかく取るのに最適なルートではありません。より特殊な型を要求しないものについては、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
については説明しませんが、他の答えはうまくいきます。
配列のメンバーにアクセスするために使用される多数を保持するか、size_t
としてバッファーを制御します。
size_t
を使用するプロジェクトの例については、 GNUのdd.c、155行目 を参照してください。
ここでは、いくつかのことを行います。彼らが皆のためであるかどうかわかりませんが、彼らは私のために働きます。
int
またはunsigned int
を直接使用しないでください。ジョブには、より適切な名前のタイプが常にあるようです。uint32_t
など)を使用します。uint_fast16_t
)を使用して、すべての配列要素にアクセスするために必要な最小サイズに基づいた型。たとえば、最大24要素を反復するfor
ループがある場合は、uint_fast8_t
を使用して、コンパイラー(またはstdint.h、取得したい知識に応じて)に決定させますこれは、その操作で最速のタイプです。それらのいずれかに同意しない場合、または推奨される代替案がある場合は、コメントでお知らせください!それがソフトウェア開発者の人生です...私たちは学び続けるか、無関係になります。
常に。
16ビットプラットフォームを使用しており、32767より大きい整数が必要な場合や、ネットワークまたはファイル内のデータ交換のために適切なバイトオーダーとサイネージを確保する必要がある場合など、より具体的なタイプを使用する特定の理由がない限り(リソースに制約がない場合は、データを「プレーンテキスト」で転送することを検討してください。つまり、ASCIIまたは必要に応じてUTF8)を意味します。
私の経験では、「intを使用するだけ」が生きるのに適した格言であり、作業しやすく、保守が容易で、正しいコードを毎回すばやく作成できることがわかりました。ただし、具体的な状況は異なる可能性があるため、このアドバイスを十分に精査してください。