49

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がunsigned型の場合、結果の値はE1に数量2を掛けてE2の累乗になり、E1の型がunsignedlongの場合は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で未定義動作を呼び出すのか、そしてなぜ右シフト演算子が実装定義の動作だけを呼び出すのかということです。

PS:「標準がそう言っているので、それは未定義の振る舞いです」のような答えを与えないでください。:P

4

8 に答える 8

39

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

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

編集:C++98の論文を見てください。署名されたタイプについてはまったく触れていません。したがって、それはまだ未定義の動作です。

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

于 2010-09-24T07:46:46.893 に答える
20

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

簡単な答えは次のとおりです。標準がそう言っているからです。

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

何らかの理由で、C ++標準化委員会は、ビット表現がどのように変化するかについて少し保証を追加したいと感じました。ただし、負の数は1の補数または符号+大きさで表される可能性があるため、結果として得られる値の可能性は依然として異なります。

16ビットintを想定すると、次のようになります。

 -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の補数マシンで符号拡張なしの右シフトは、特に有用ではありません。したがって、現在の状態は、保証されていなくても、符号拡張を行うマシンのための余地を残すため、空のビットをゼロで埋める必要があるよりも間違いなく優れています。

于 2010-09-24T18:02:26.850 に答える
7

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

値が正または0の場合の左シフト演算の場合、2の累乗の乗算としての演算子の定義は理にかなっているため、結果がオーバーフローしない限り、すべてが問題ありません。驚くことではありません。

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

私の結論:

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

    i *(1u << k)

いずれの場合も、コンパイラはこれを適切なアセンブラに変換します。

于 2010-09-24T08:18:20.047 に答える
3

これらの種類の多くは、一般的なCPUが1つの命令で実際にサポートできるものと、追加の命令が必要な場合でもコンパイラー作成者が保証するのに十分役立つものとの間のバランスです。一般に、ビットシフト演算子を使用するプログラマーは、そのような命令を使用してCPU上の単一の命令にマップすることを期待します。そのため、CPUが動作を強制して操作するのではなく、「エッジ」条件をさまざまに処理する未定義または実装の動作があります。予想外に遅くなります。より単純なユースケースでも、追加の事前/事後または処理手順が作成される場合があることに注意してください。

于 2010-09-24T07:35:40.120 に答える
1

私の質問は、なぜ左シフト操作がCで未定義動作を呼び出すのか、そしてなぜ右シフト演算子が実装定義の動作だけを呼び出すのかということです。

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

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

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

2つ目の理由は、2のコンプリメントマシンでの潜在的な符号の変更だと思います。しかし、私はそれをどこでも読んだことがありません(@sellibitzeに不快感はありません(そして私はたまたま彼に同意します))。

于 2014-04-05T17:03:50.817 に答える
0

C++03の動作はC++11およびC99の場合と同じであり、左シフトのルールを超えて調べる必要があります。

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

式の評価中に、結果が数学的に定義されていないか、そのタイプの表現可能な値の範囲内にない場合、動作は定義されていません

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

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

于 2014-07-19T20:06:33.510 に答える
0

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

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

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

そのような最適化が本当に役立つ頻度については疑問ですが、実際には、そのような有用性の希少性は、動作を未定義のままにしておくことに有利に働きます。2の補数の実装が一般的な方法で動作しない唯一の状況が、最適化が実際に役立つ状況であり、そのような状況が実際に存在しない場合、実装は、マンデートの有無にかかわらず、一般的な方法で動作します。行動を義務付ける必要があります。

于 2015-04-17T23:43:26.603 に答える
-2

シフトの結果は、数値表現によって異なります。数値が2の補数として表される場合にのみ、シフトは乗算のように動作します。しかし、問題は負の数に限ったことではありません。8を超えて表される4ビットの符号付き数値(別名オフセット2進数)について考えてみます。数値1は1+8または1001として表されます。これをビットとして左シフトすると、-6の表現である0010が得られます。同様に、-1は-1 + 8 0111として表され、左シフトすると1110になり、+6を表します。ビット単位の動作は明確に定義されていますが、数値の動作は表現システムに大きく依存しています。

于 2016-03-29T21:09:28.800 に答える