整数型および整数型に関連した紛らわしい挙動
テクニカル・ノート 12582
アーキテクチャ:
All
コンポーネント:
general
更新日:
2018/08/07 8:52
はじめに
ISO/ANSIの標準Cは、混乱を招く動作をする場合があります。本テキストでは、規格を参照しながらその例をいくつか紹介し、技術的な背景についても少し説明します。
汎整数拡張
6.2.1.1によれば、「char型、short int型、int型のビットフィールド、またはそれらの符号付きおよび符号なしのバージョン、または列挙型」は、式で使用されたとき、汎整数拡張によってint型またはunsigned int型に変換されます。ほとんどの場合、これによって、式から数学的に得られるであろうと期待される結果が得られます。例:
char c1,c2,c3;
c1=0xA
c2=0xB;
c3=c1+c2;
16ビットのint型を持つコンパイラの場合、定義に基づいて、int型の加算が行われた後に、その結果がchar型に切り詰められます。0x0A + 0x0B --> 0x000A+ 0x000B --> 0x0015 --> 0x15.
この例では、結果として得られる値が代入先の型に収まっていることに注目してください。結果として得られる値が代入先の型に収まらない場合、状況はやや複雑になります。最適化コンパイラを使用すると、これと似たようなほとんどの場合において、実際の演算は最も低い精度で実行されます(そうした演算が結果に影響を及ぼさない状況については、後述する「サイズの最適化」を参照してください)。
整数定数型
6.1.3.2によれば、サフィックスを持たない整数定数は、実際に記述された値と基数によって型が決定され、16進数定数は、int型、unsigned int型、long int型、unsigned long int型のうち最初に値を表現できる型がその定数の型になります。ここで興味深いのは、最小の型がchar型ではなくint型であることです。例:
char c1;
c1=0xA
16ビットのint型を持つコンパイラの場合、代入を行うには値を切り詰める必要があります。0xAは実際には0x000Aであるため、char型に収めるにはこの値を切り詰めなければなりません。前述したように、最適化コンパイラを使用すると、これと似たようなほとんどの場合において、実際の演算は最も低い精度で実行されます(そうした演算が結果に影響を及ぼさない状況については、後述する「サイズの最適化」を参照してください)。
サイズの最適化
実際に式を評価する際、結果が同じであるならば、コンパイラは他の精度を自由に使うことができます。セクション5.1.2.3を参照してください。特に、"規格に準拠した実装のための最低要件:"の後にある要点リストを参照してください。ここに最低要件についての概略が示されています。興味深い例も記載されています。
混乱を招く動作の例
整数型とその変換の規則が、混乱を招く動作の原因となることがあります。注意すべき点は、異なるサイズの型や論理演算(特にビット否定)が関係する代入文や条件文(評価式)です。ここでいう型には、定数の型も含まれます。
ワーニング(定数の条件文や無意味な比較など)が発生する場合もあれば、予期した結果と異なるだけの場合もあります。コンパイラが定数の条件文のインスタンスを特定するために最適化を使用する場合などは、より高水準の最適化でのみ、コンパイラがワーニングを生成することがあります。
例1:
8ビットのchar型、16ビットのint型、2の補数を扱う場合を考えます。
void f1(unsigned char c1)
{
if (c1 == ~0x80)
;
}
この場合、評価結果は常にfalseになります。
説明: 右辺: 0x80は実際には0x0080であるため、~0x0080は0xFF7Fとなります。左辺: c1は8ビットのunsigned char型であるため、その値が255より大きくなることはありません。また、負の値にもならないため、汎整数拡張された値の上位8ビットが設定されることはありません。
例2:
8ビットのchar型、16ビットのint型、2の補数を扱う場合を考えます。
void f2(void)
{
char c1;
c1= ~0x80;
if (c1 == ~0x80)
;
}
この場合も、代入時にint型のオブジェクトに対してビット反転が行われます(すなわち、~(0x0080) --> 0xFF7Fとなります)。そして、その結果がchar型に代入されます。すなわち、このchar型は(正の)0x7Fという値になります。条件文では、その結果が汎整数拡張されて0x007Fになります。そして、この値が0xFF7Fと比較され、その結果はfalseとなります。サフィックスなしのchar型が符号付きとして扱われ、なおかつその定数の最上位ビットが0であった場合(すなわち、0x00~0x7Fのいずれかの値であった場合)、ビット反転した結果は負の値となり、条件文の判定はtrueになります(下記を参照)。
例3:
8ビットのchar型、16ビットのint型、2の補数を扱う場合を考えます。
void f3(void)
{
signed char c1;
c1= ~0x70;
if (c1 == ~0x70)
;
}
この場合も、代入時にint型のオブジェクトに対してビット反転が行われます(すなわち、~(0x0070) --> 0xFF8Fとなります)。そして、その結果がchar型に代入されます。すなわち、このchar型は0x8Fという値になります。条件文では、その結果が汎整数拡張/符号拡張されて0xFF8Fになります。これは期待したとおりの値であり、条件文の判定は正しく機能します。
例4:
8ビットのchar型、16ビットのint型、2の補数を扱う場合を考えます。
void f4(void)
{
signed char c1;
signed int i1;
i1= 0xFF;
c1= 0xFF;
if (c1 == i1)
;
}
最初の代入では、i1の値は255になります。一方、2番目の代入(c1への代入)では、255という値は代入先の符号付きchar型に収まりません。そのため、この判定が正しく機能するという前提を置くことはできません。この例では、c1は符号付きchar型であることが明確に宣言されているので問題は極めて明白ですが、サフィックスなしのchar型が使用され、なおかつそれが符号付きのchar型として扱われる場合は、この問題を見つけるのはもっと難しくなります。
例5:
void CompilerTest(void)
{
int64_t a,b;
int16_t c,d,e;
c=0x789F;
d=0x7FFF;
e=0x7777;
a=c*d*e; //Wrong result: 0xFFFFFFFFC52A8517
b=c*d;
b=b*e; //Correct result: 0x00001C24C52A8517
}
「(c*d)*e」という式は、汎整数拡張されて「((int) c * (int) d) * (int) e」になります。この式の結果は、暗黙のうちにlong long型に変換されてからaに格納されます。
long long型の演算を強制する方法の一つとして、内側にある演算で使用されているいずれかのオペランドを明示的に変換する方法があります。例えば、dを「(c*(long long)d)*e」のように変換します。これにより、内側にある演算がlong long型で実行され、long long型の結果が得られます。さらに、この結果はその外側にある演算のオペランドとなるため、外側にある演算がlong long型で実行されます。
全ての製品名は、それぞれの所有者の商標または登録商標です。