この小さな宝石の後ろに隠れている非常に厄介なバグを見つけました。 C++仕様では、符号付きオーバーフローは未定義の動作ですが、値がビット幅sizeof(int)
に拡張されたときにオーバーフローが発生する場合のみです。私が理解しているように、char
のインクリメントは、sizeof(char) < sizeof(int)
である限り、未定義の動作であってはなりません。しかし、それはc
がimpossible値を取得する方法を説明しません。 8ビット整数として、どのようにしてc
がそのビット幅より大きい値を保持できますか?
// Compiled with gcc-4.7.2
#include <cstdio>
#include <stdint.h>
#include <climits>
int main()
{
int8_t c = 0;
printf("SCHAR_MIN: %i\n", SCHAR_MIN);
printf("SCHAR_MAX: %i\n", SCHAR_MAX);
for (int32_t i = 0; i <= 300; i++)
printf("c: %i\n", c--);
printf("c: %i\n", c);
return 0;
}
SCHAR_MIN: -128
SCHAR_MAX: 127
c: 0
c: -1
c: -2
c: -3
...
c: -127
c: -128 // <= The next value should still be an 8-bit value.
c: -129 // <= What? That's more than 8 bits!
c: -130 // <= Uh...
c: -131
...
c: -297
c: -298 // <= Getting ridiculous now.
c: -299
c: -300
c: -45 // <= ..........
未定義の動作に対して不可能な結果を得ることは有効な結果ですが、実際にはコードには未定義の動作はありません。何が起こっているのかというと、コンパイラは動作を未定義と考えており、それに応じて最適化します。
c
がint8_t
として定義され、int8_t
がint
にプロモートされる場合、c--
は、int
算術演算でc - 1
の減算を実行し、結果をint8_t
に変換します。 int
の減算はオーバーフローせず、範囲外の整数値を別の整数型に変換することは有効です。宛先タイプが署名されている場合、結果は実装定義ですが、宛先タイプの有効な値でなければなりません。 (また、宛先タイプが符号なしの場合、結果は明確に定義されますが、ここでは適用されません。)
コンパイラには、他の要件があるため、標準に準拠していない以外のバグがある場合があります。コンパイラは、それ自体の他のバージョンと互換性がある必要があります。また、他のコンパイラと何らかの方法で互換性があり、そのユーザーベースの大部分が保持する動作に関するいくつかの信念に準拠することも期待される場合があります。
この場合、それは適合バグのようです。式c--
は、c = c - 1
と同様の方法でc
を操作する必要があります。ここでは、右側のc
の値がint
型に昇格され、その後減算が行われます。 c
はint8_t
の範囲にあるため、この減算はオーバーフローしませんが、int8_t
の範囲外の値を生成する場合があります。この値が割り当てられると、変換はint8_t
型に戻されるため、結果はc
に戻ります。範囲外の場合、変換には実装定義の値があります。 ただし、int8_t
の範囲外の値は実装定義の有効な値ではありません。実装は、8ビット型が突然9ビット以上を保持することを「定義」できません。値の場合実装定義であるとは、int8_t
の範囲内の何かが生成され、プログラムが継続することを意味します。 C標準では、飽和演算(DSPで一般的)またはラップアラウンド(メインストリームアーキテクチャ)などの動作が可能です。
コンパイラは、int8_t
やchar
などの小さな整数型の値を操作するときに、より広い基礎となるマシン型を使用しています。算術が実行されると、短整数型の範囲外の結果は、この幅の広い型で確実にキャプチャできます。変数が8ビット型であるという外部から見える動作を維持するには、より広い結果を8ビットの範囲に切り捨てる必要があります。マシンの保存場所(レジスタ)は8ビットより広く、大きな値に満足しているため、これを行うには明示的なコードが必要です。ここでは、コンパイラ正規化を無視値をそのままprintf
に渡します。 printf
の%i
変換指定子は、引数が元々int8_t
計算に由来することを知りません。 int
引数を使用しているだけです。
これをコメントに収めることができないため、回答として投稿しています
非常に奇妙な理由で、--
演算子が原因であることがあります。
Ideoneに投稿されたコードをテストし、c--
をc = c - 1
に置き換えました。値は[-128 ... 127]の範囲内に留まりました。
c: -123
c: -124
c: -125
c: -126
c: -127
c: -128 // about to overflow
c: 127 // woop
c: 126
c: 125
c: 124
c: 123
c: 122
気紛れ? i++
やi--
のような式に対してコンパイラーが何をするかについてはあまり知りません。戻り値をint
に昇格して渡す可能性があります。あなたが実際に8ビットに収まらない値を取得しているので、それが私が思いつくことができる唯一の論理的な結論です。
基礎となるハードウェアは、まだint8_tを保持するために32ビットレジスタを使用していると思います。仕様ではオーバーフローの動作が強制されないため、実装ではオーバーフローのチェックは行われず、より大きな値も保存できます。
ローカル変数をvolatile
としてマークすると、そのためにメモリを使用することを強制し、その結果、範囲内の期待値を取得します。
アセンブラーコードは問題を明らかにします:
:loop
mov esi, ebx
xor eax, eax
mov edi, OFFSET FLAT:.LC2 ;"c: %i\n"
sub ebx, 1
call printf
cmp ebx, -301
jne loop
mov esi, -45
mov edi, OFFSET FLAT:.LC2 ;"c: %i\n"
xor eax, eax
call printf
EBXはFFポストデクリメントでandedするか、EBXの残りをクリアしてBLのみを使用する必要があります。 decの代わりにsubを使用することに興味があります。 -45は完全に神秘的です。 300と255 = 44のビット単位の反転です。-45=〜44。どこかに接続があります。
C = c-1を使用してさらに多くの作業を行います。
mov eax, ebx
mov edi, OFFSET FLAT:.LC2 ;"c: %i\n"
add ebx, 1
not eax
movsx ebp, al ;uses only the lower 8 bits
xor eax, eax
mov esi, ebp
次に、RAXの低い部分のみを使用するため、-128〜127に制限されます。コンパイラオプション "-g -O2"。
最適化なしで、正しいコードが生成されます。
movzx eax, BYTE PTR [rbp-1]
sub eax, 1
mov BYTE PTR [rbp-1], al
movsx edx, BYTE PTR [rbp-1]
mov eax, OFFSET FLAT:.LC2 ;"c: %i\n"
mov esi, edx
したがって、オプティマイザーのバグです。
使用する %hhd
の代わりに %i
!あなたの問題を解決するはずです。
コンパイラーの最適化の結果は、32ビットの数値を出力するようにprintfに指示し、(実際にはポインターサイズの)スタックに(おそらく8ビットの)数値をプッシュすることと組み合わせた結果です。
これはコードの最適化によって行われていると思います:
_for (int32_t i = 0; i <= 300; i++)
printf("c: %i\n", c--);
_
コンパイラーは、i
とc
の両方に_int32_t i
_変数を使用します。最適化をオフにするか、直接キャストしますprintf("c: %i\n", (int8_t)c--);
c
自体はint8_t
として定義されていますが、++
または--
over int8_t
を操作する場合、最初にint
と操作の結果に暗黙的に変換されます代わりにcの内部値がprintfで出力されますが、これはたまたまint
になります。
ループ全体の後のc
の実際の値、特に最後のデクリメントの後を参照してください
-301 + 256 = -45 (since it revolved entire 8 bit range once)
動作に似た正しい値-128 + 1 = 127
c
は、int
サイズのメモリの使用を開始しますが、int8_t
のみを使用してそれ自体として印刷すると、8 bits
として印刷されます。 int
として使用される場合、すべての32 bits
を利用します
[コンパイラのバグ]
Int iが300になり、cが-300になるまでループが続くためだと思います。そして最後の価値は
printf("c: %i\n", c);