不具合を生みやすい汎整数拡張の注意すべき振る舞い

プログラマとして、ISO/ANSI C言語規格について、全てを把握する必要はありません。アプリケーションは、規格の全てを知っていなくても開発することができます。しかし、規格というものは、言語の詳細を把握するのに、特にC言語の隅々まで探求したい時に、大きな助けとなります。本稿では、不具合を生みやすい汎整数拡張の注意点をご紹介します。

言語の規格を読むとき、紙面で明確と思える点がある一方、そうでない部分も多々あります。規格のいくつかの部分は、完全に振る舞いを理解し、活用する前に、深く分析をする必要があります。以下では、Cの隅をついているとは言えないような、実際のアプリケーションの中に見られる紛らわしい振る舞いの例を示します。ほとんどの場合、そうしたものは規格が実際には何を言っているのかを分析することで、予測できます。

最適化コンパイラの使用

組込みソフトの多くで、コードサイズは極めて重要で、データ型の選択が最終的なコードサイズに大きく影響します。

標準ヘッダファイルであるlimits.hは、汎整数型の最小値および最大値を定義します。規格によれば、コンパイラは、実際に式を評価する際に、結果が同じであれば、異なる精度を自由に使用することができます。これは、IAR C/C++ コンパイラのような最適化コンパイラでは、ほとんどの場合、低い精度で演算するコードが出力されることを意味ます。

このことは規格の5.2.4.2.1項で説明され、最低限の要求事項が記載されています。不具合を回避するため、記載されている例も読む価値があります。

混乱しそうな振る舞いを深堀する前に、汎整数拡張と整数定数について規格がどういっているかを見てみましょう。

汎整数拡張

C言語規格6.3.1.1項、intが元の型のすべての値を表現できる場合はintに変換され、そうでない場合はunsigned intに変換されます。これらを汎整数拡張と呼びます。汎整数拡張では、符号を含めた値が保持されます。

6.3.1.1項によると、bool・char・signedまたはunsigned char・short int・列挙型は、式の中で使用されると、汎整数拡張により、intまたはunsigned intに格上げされます。 ほとんどの場合、数学的に期待したとおりの結果が得られます。

char c1,c2,c3;
c1=0xA
c2=0xB;
c3=c1+c2;

16bitのint型のコンパイラでは、定義によると、intの加算となり、その結果をcharへ切り詰めることになります。0x0A + 0x0Bから、0x000A+ 0x000Bとなり、0x0015 を0x15へ変換するという流れです。

この例では、結果として得られる値が代入先の型に適合していることに注意してください。そうでない場合、状況は少し複雑になります。最適化コンパイラを使用すると、同じようなケースでも実際には低い方の精度で演算が行われます。

整数定数

6.4.4.1項によると、整数定数の型は、サフィックス(例えば、u・Uがunsigned、l・Lがlong、ll・LLがlong longなどです)および基数(10進数、8進数、16進数)で与えられます。サフィックスなしの16進数の定数は、その定数が収まる一番近い型となります。intまたはunsigned intまたはlong intまたはunsigned long intまたはlong long intまたはunsigned long long intです。興味深いのは最も小さい型がcharではなくintである点です。

char c1; 
c1=0xA

16bitのint型を持つコンパイラでは、代入には切り捨てが必要です。0xAは0x000Aとなり、charに収まるように切り詰めなければなりません。繰り返しになりますが、最適化されたコンパイラを使えば、ほとんどの類似したケースでは、実際に低い方の精度で演算が行われます。

不具合を生みやすい混乱を招く振る舞い

整数型と型変換のルールは、いくつか混乱を招くパタンがあります。例えば、異なるサイズの型や論理演算(特にビット否定)を含む代入や条件式などです。ここでの型には、定数の型も含まれます。

警告がでる場合もありますが(例えば、一定の条件や、無駄な比較など)、期待と異なる結果が生じるだけの場合もあります。特定の状況下では、コンパイラがより高い最適化でのみ警告を発することがあります。例えば、コンパイラが定数条件式の一部のインスタンスを識別するために最適化に依存している場合などです。

例1:

以下は8bit char、16bit int、2の補数を想定します。

void f1(unsigned char c1) 
{
if (c1 == ~0x80)
;
}

上記テストは常に不成立となります。
理由:右辺の0x80は0x0080に汎整数拡張され、~0x0080は0xFF7Fとなります。左辺c1は8bitのunsigned charで、255以下であり正の整数です。このため、汎整数拡張された値は決して、8bitの最大の値以上を持ちません。

例2:

以下は8bit char、16bit int、2の補数を想定します。

void f2(void) 
{
char c1;
c1= ~0x80;
if (c1 == ~0x80)
;
}

まずビット否定はintに対して行い、つまり~(0x0080)が0xFF7Fとなります。この値がcharに代入される、つまり、charは正の値である0x7Fとなります。 条件式では、再び汎整数拡張で0x007Fとなり、この値が0xFF7Fと比較されます。テストは成立しません。特に単純なcharが符号付きの場合、かつ定数の最上位bitが0(0x00から0x7Fの値)の場合、ビット否定の値は負の値でテストは成立する可能性があります。

例3:

以下は8bit char、16bit int、2の補数を想定します。

void f3(void)
{
signed char c1;
c1= ~0x70;
if (c1 == ~0x70)
;
}

代入では,intのオブジェクトに対してビット否定が行われます。つまり0xFF8Fとなります。この値はcharに代入され、charは0x8Fの値を持つことになります。条件式では、この値が汎整数拡張されて0xFF8Fとなり、テストは成立します。

例4:

以下は8bit char、16bit int、2の補数を想定します。

void f4(void) 
{
signed char c1;
signed int i1;
i1= 0xFF;
c1= 0xFF;
if (c1 == i1)
;
}

1つ目の代入でi1は255になりますが、2つ目の代入のc1には255がsigned charに収まりません。したがって、このテストが成功することを当てにできません。この問題はc1に明示的にsigned charを使用した場合には明白ですが、デフォルトでsigned charである単純なcharを使用した場合、検出がより困難になります。

 

参考文献

The C standard, Incorporating Technical Corrigendum 1, BS ISO/IEC 9899:1999