web-dev-qa-db-ja.com

左側のオペランドが負の値の場合、左シフト演算が未定義の動作を呼び出すのはなぜですか?

Cでは、ビットごとの左シフト演算は、左側のオペランドが負の値の場合に未定義の動作を呼び出します。

ISO C99からの関連する引用(6.5.7/4)

E1 << E2の結果は、E1の左シフトされたE2ビット位置です。空のビットはゼロで埋められます。 E1が符号なしタイプの場合、結果の値はE1×2です。E2、結果の型で表現可能な最大値より1を法として減じられます。 E1に符号付きの型と非負の値があり、かつE1×2の場合E2 結果の型で表現できる場合、それが結果の値になります。それ以外の場合、動作は未定義です。

しかし、C++では動作が明確に定義されています。

ISO C++-03(5.8/2)

E1 << E2の値は、E1(ビットパターンとして解釈)の左シフトE2ビット位置です。空のビットはゼロで埋められます。 E1が符号なしの型である場合、結果の値はE1に数量2を乗じてE2を乗じたものであり、E1がunsigned long型の場合はULONG_MAX + 1を法として減じ、それ以外の場合はUINT_MAX + 1になります。 [注:定数ULONG_MAXおよびUINT_MAXはヘッダーで定義されています)。 ]

つまり

int a = -1, b=2, c;
c= a << b ;

cで未定義の動作を呼び出しますが、動作はC++で適切に定義されています。

ISO C++委員会に、Cでの動作とは対照的に、明確に定義された動作を検討させたのはなぜですか。

一方、動作はimplementation definedは、左のオペランドが負のときのビット単位の右シフト演算です。

私の質問は、なぜ左シフト操作がCで未定義の動作を呼び出すのか、なぜ右シフト演算子は実装定義の動作だけを呼び出すのか?

追伸:「標準で規定されているため、未定義の動作です」などの回答は行わないでください。 :P

44
Prasoon Saurav

コピーした段落は、符号なしの型について話しています。動作is C++では未定義。最後のC++ 0xドラフトから:

E1 << E2の値は、E1の左シフトされたE2ビット位置です。空のビットはゼロで埋められます。 E1が符号なしタイプの場合、結果の値はE1×2です。E2、結果の型で表現可能な最大値より1を法として減じられます。それ以外の場合、E1に符号付きの型と負でない値があり、E1×2E2 結果の型で表現できる場合、それが結果の値になります。 それ以外の場合、動作は未定義

編集:C++ 98ペーパーを見てください。署名された型についてはまったく触れられていません。したがって、それは未定義の動作です。

右シフトネガティブは、実装で定義されています。どうして?私の意見では、左の問題からの切り捨てがないため、実装を定義するのは簡単です。左にシフトするときは、右からシフトしたものだけでなく、残りのビットで何が起こるかを言う必要があります。 2の補数表現で、これは別の話です。

33
ybungalobill

Cでは、ビットごとの左シフト演算は、左側のオペランドが負の値の場合に未定義の動作を呼び出します。 [...]しかし、C++では動作が明確に定義されています。 [...] なぜ [...]

簡単な答えは、標準がそう言っているからです。

より長い答えは:CとC++の両方が2の補数以外の負の数の他の表現を許可するという事実とおそらく関係があります。何が起こるかについての保証を少なくすると、あいまいなマシンや古いマシンを含む他のハードウェアで言語を使用できるようになります。

何らかの理由で、C++標準化委員会は、ビット表現がどのように変化するかについて少し保証を加えるように感じました。ただし、負の数は1の補数または符号+マグニチュードで表すことができるため、結果の値の可能性は変わります。

16ビット整数を仮定すると、

 -1 = 1111111111111111  // 2's complement
 -1 = 1111111111111110  // 1's complement
 -1 = 1000000000000001  // sign+magnitude

左に3シフトすると、

 -8 = 1111111111111000  // 2's complement
-15 = 1111111111110000  // 1's complement
  8 = 0000000000001000  // sign+magnitude

ISO C++委員会に、Cでの動作とは対照的に、明確に定義された動作を検討させたのはなぜですか。

私は彼らがこの保証をしたので、あなたが何をしているのか知っているとき(つまり、あなたのマシンが2の補数を使うことが確実なとき)に<<を適切に使うことができると思います。

一方、動作は、左のオペランドが負の場合のビット単位の右シフト演算に対して定義された実装です。

標準を確認する必要があります。しかし、あなたは正しいかもしれません。 2の補数マシンでの符号拡張なしの右シフトは、特に有用ではありません。したがって、現在の状態は、保証されていない場合でも、符号拡張を行うマシンのための余地を残しているため、空のビットをゼロで埋める必要があるよりも間違いなく優れています。

17
sellibitze

タイトルに記載されている本当の質問に答えるために:符号付きの型の演算については、数学演算の結果がターゲットの型に適合しない場合(アンダーまたはオーバーフロー)、これは未定義の動作をします。符号付き整数型はそのように設計されています。

値が正または0の場合の左シフト演算では、2のべき乗の乗算としての演算子の定義が意味をなすため、結果がオーバーフローしない限り、何も問題はありません。

値が負の場合、2のべき乗で同じ乗算の解釈を持つことができますが、ビットシフトの観点から考えると、これはおそらく驚くべきことです。明らかに、標準化委員会はそのようなあいまいさを避けたかった。

私の結論:

  • 実際のビットパターン操作を実行する場合は、符号なしの型を使用します
  • 値(符号付きかどうか)に2の累乗を掛けたい場合は、次のようにします。

    i *(1u << k)

あなたのコンパイラは、どんな場合でもこれをまともなアセンブラに変換します。

7
Jens Gustedt

これらの種類の多くは、一般的なCPUが単一の命令で実際にサポートできるものと、追加の命令を受け取った場合でもコンパイラライターが保証することを期待するのに十分役立つものとの間のバランスです。一般に、ビットシフト演算子を使用するプログラマーは、CPU上の単一の命令にそのような命令をマップすることを期待しているため、動作を強制して操作するのではなく、CPUが「エッジ」条件のさまざまな処理を行う未定義または実装の動作があります。意外と遅くなる。単純なユースケースであっても、追加の事前/事後または取り扱いの説明が作成される場合があることに注意してください。一部のCPUがトラップ/例外/割り込み(C++のtry/catchタイプの例外とは異なります)または一般的に役に立たない/説明できない結果を生成した場合、未定義の動作が必要になる可能性があります。少なくともいくつかの定義された動作、そしてそれらは振る舞いの実装を定義することができます。

3
Tony Delroy

私の質問は、なぜ左シフト操作がCで未定義の動作を呼び出すのか、なぜ右シフト演算子は実装定義の動作だけを呼び出すのか?

LLVMの人々は、命令がさまざまなプラットフォームに実装される方法のため、シフト演算子には制約があると推測しています。から すべてのCプログラマが未定義の動作#1/3について知っておくべきこと

...私の推測では、これはさまざまなCPUの基になるシフト操作がこれで異なることを行うために発生したと考えられます。たとえば、X86は32ビットのシフト量を5ビットに切り捨てます(したがって、32ビットのシフトはシフトと同じです)ただし、PowerPCは32ビットのシフト量を6ビットに切り捨てます(したがって、32シフトするとゼロになります)。これらのハードウェアの違いにより、動作はCでは完全に定義されていません...

議論はレジスターサイズよりも大きい量をシフトすることについてであったとネイト。しかし、それは私が権威からのシフトの制約を説明するのに私が見つけた最も近いものです。

think2番目の理由は、2の補数マシンでの符号の変化の可能性です。しかし、私はそれをどこでも読んだことがありません(@sellibitzeに不快感はありません(そして私はたまたま彼に同意します))。

1
jww

C89では、符号付き整数型と符号なし整数型でパディングビットを使用しない2の補数プラットフォームで、左シフトの負の値の動作が明確に定義されていました。符号付き型と符号なし型の共通の値ビットは同じ場所にあり、符号付き型の符号ビットが移動できる唯一の場所は、符号なし型の上位値ビットと同じ場所でした。他のすべての左側にあります。

C89の必須の動作は、少なくとも乗算として処理してもオーバーフローが発生しない場合は、パディングなしの2の補数プラットフォームで有用かつ賢明でした。他のプラットフォーム、または符号付き整数オーバーフローを確実にトラップしようとする実装では、動作が最適ではなかった可能性があります。 C99の作成者は、C89の規定された動作が理想的ではなかった場合に実装の柔軟性を許可したいと考えていますが、理論的根拠には、高品質の実装が存在した場合に古い方法で動作し続けるべきではないという意図はありません。それ以外の場合にやむを得ない理由はありません。

残念ながら、2の補数の計算を使用しないC99の実装はこれまでありませんでしたが、C11の作成者は一般的なケース(オーバーフローではない)の動作を定義することを拒否しました。 IIRC、主張はそうすることは「最適化」を妨げるだろうということでした。左シフト演算子を使用すると、左オペランドが負の場合に未定義の動作が呼び出され、コンパイラは左オペランドが負でない場合にのみシフトに到達できると想定できます。

私はそのような最適化がどれほど頻繁に本当に役立つかについて疑わしいですが、そのような有用性の希少性は実際には振る舞いを未定義のままにすることを優先します。 2の補数の実装がありふれた方法で動作しない唯一の状況が最適化が実際に役立つ状況であり、そのような状況が実際に存在しない場合、実装は委任の有無にかかわらずありふれた方法で動作し、行動を強制する必要があります。

0
supercat

C++ 03での動作はC++ 11およびC99での動作と同じです。左シフトの規則を超えて見ればよいだけです。

規格のセクション5p5は次のように述べています:

式の評価中に結果が数学的に定義されていないか、その型の表現可能な値の範囲にない場合、動作は未定義です。

C99およびC++ 11で未定義の動作として具体的に呼び出されている左シフト式は、表現可能な値の範囲外の結果に評価される式と同じです。

実際、モジュラー演算を使用した符号なし型に関する文は、表現可能な範囲外の値を生成しないようにするためのものです。これは自動的に未定義の動作になります。

0
Ben Voigt