型変換と暗黙的なキャストで起きるバグ

C言語において型変換は基本的な仕組みですが、コンパイル時におこなわれる暗黙的なキャストは意図しない動作につながります。この記事では暗黙的なキャストによって発生する代表的なバグのパターンと、その回避策を整理します。

暗黙的なキャストとは

明示のキャストと暗黙のキャストの違いを示した図。左は明示のキャストで、(int)xのようにプログラムに記載して変換する型を明確に指定する。右は暗黙のキャストで、コンパイラが自動で型変換して計算を実行する。中央はa + bの計算にあたってint型がdouble型へそろえられる様子を示し、型をそろえる必要があることと、意図しない変換がバグの原因になることを示している。

C言語で異なる型同士の演算や代入をおこなうときには型変換で型をそろえる必要があります。プログラムに明確に記載されたキャストを「明示的なキャスト」と呼び、小数点以下の数字まで使って計算したい場合や、文字を数字にしたい場合などに使われます。

明示的なキャストがされておらず、右辺と左辺の型が異なるときはコンパイラが自動的に型変換を実施します。この自動的な型変換を「暗黙的なキャスト」と呼びます。自動で型がそろい、計算を進めていけるため便利ですが、意図しない型変換が発生するとバグの原因になりかねません。

バグを防ぐためには暗黙的なキャストで起きやすいケースを把握し、明示的なキャストで回避することが求められます。

意図しない変換によるバグ

暗黙的なキャストで発生しやすい意図しない型変換を列挙します。

符号拡張

符号拡張とは符号付きの整数をより大きな型に変換する際に、空いた上位ビットを0や1で埋めることをいいます。符号が保たれるように最上位ビットと同じ値で埋められるのが一般的です。符号付きからより広い符号付き型では一般に符号拡張(最上位ビットで埋める)、符号なし型ではゼロ拡張(0で埋める)が起きます。これが混ざると、負値が巨大な正の値に見えることがあります。変換する型によっては、変換後に期待しない値になることがあります。

たとえば、signed char型をint型に変換する場合、符号ビットは保持されます。この値をさらにunsigned int型に変換すると、C言語の規則により2^Nを法とする値になります。

なお、char(signed/unsignedを付けないplain char)が符号付きか符号なしかは処理系定義(implementation defined)で、環境によって異なります。移植性が必要ならsigned char/unsigned charを明示します。組み込み向け処理系では符号なしが既定のケースも少なくありません。符号の有無に依存する処理では、signed charまたはunsigned charの明示が不可欠です。

また、このような符号拡張により値が変化した結果、switch文のcaseラベルと一致しなくなり、条件分岐に入るはずの処理が期待どおり動かないバグを引き起こすこともあります。例として次のような形が挙げられます。

char x = -1;
switch ((unsigned char)x) { case 255: ... }

整数拡張

C言語では、整数拡張(integer promotions)として、(1)まずintへ拡張(入らない場合はunsigned int)、(2)その後に必要なら「通常の算術変換(usual arithmetic conversions)」で型をそろえます。これはCの整数拡張(integer promotions)によるもので、char/short等は演算時にint(またはunsigned int)へ昇格します。両辺が同じ型であっても発生する点に注意が必要です。

char型やshort型は一度int型に変換されてから演算されるため、計算結果を元の型に戻すとき、上位ビットの切り捨てや符号の変換を起こし、意図しない値になります。

たとえばuint8_t a=250, b=10; uint8_t c=a+b;のように、演算自体がintでおこなわれると、代入時に上位ビットの切り捨てが起こります。

特に符号変換が起きる場合は、負の値を代入するテストケースを用意しないと検出できません。整数拡張が起きる箇所を念頭にテストを設計しないと、このバグは検出しにくくなります。

符号付き整数と符号なし整数の比較

符号付き整数と符号なし整数を比較すると、符号なし側の型ランクが符号付き側と同等以上の場合は、符号付き整数が符号無し型へ変換されます(符号付き側のランクが高く、かつ符号なし型の全値を表現できる場合は逆方向の変換が起きます)。多くのケースで signedがunsignedに変換されます。特にsize_t(符号なし)との比較で問題が起きやすいため注意が必要です。

このとき符号付きの値が負であれば、変換後は非常に大きな値になります。そのため、「負の値のほうが大きい」と判定され、意図した分岐に入らないバグの原因になりかねません。大小比較で意図した分岐に入らない場合、符号なし整数の暗黙的なキャストが起きている可能性を疑うべきです。

バグを防ぐための設計

暗黙的なキャストで起きる事象はある程度予想できるため、コードの書き方やコンパイル方法の工夫で防げます。

型の取り扱い

明示的なキャストを使用することで暗黙的なキャストを防げると同時に、コードで変換したい型を明確に示せます。符号付き整数と符号なし整数の混在や、整数拡張が起きるケースなど、注意すべきところで可読性を上げられます。

しかし、余計なキャストが増えると、コードが見づらくなります。明示的なキャストは意図をもって適切に使用することが大切です。

コンパイラ警告を有効にする

警告オプションを有効にしてコンパイルすることで、異なる型同士の比較など、暗黙的なキャストが起きていそうな箇所を検出できます。たとえばGCCの-Wallは、代表的な警告を有効化します(有効になる範囲はバージョンにより差があります)。

ただし暗黙変換の危険度が高い警告(符号変換・情報落ち)は別途-Wextraや-Wconversion、-Wsign-conversionなどの追加指定が必要になることがあります。符号付き/符号なし比較は-Wsign-compare(多くの場合-Wallで有効)も確認対象です。利用可能なオプションはコンパイラにより異なります。

ただしコンパイル時に大量の警告が出るため、キャストに問題が無いことを確認する間だけ有効にするのも効果的です。

コーディング規約を利用する

コーディング規約にのっとりコードを書くことでもバグは防げます。C言語のコーディング規約であるMISRA Cでは式の本質型(essential type)に基づき、異なるカテゴリ間の暗黙変換や、幅の異なる型同士の演算を制限します。開発中のコードがコーディング規約に準拠しているかを解析するツールもあり、効率の良い開発をサポートしてくれます。

組み込みソフトの世界 トップへ戻る