C++11 では Undefined Behaviorですが、C の場合while(1);
は Undefined Behavior ですか?
4 に答える
それは明確に定義された動作です。C11 では、新しい条項 6.8.5 ad 6 が追加されました
制御式が定数式ではない反復ステートメント156)入出力操作を実行せず、揮発性オブジェクトにアクセスせず、その本体、制御式、または (for の場合は for statement) その式-3 は、実装によって終了すると想定される場合があります。157)
157)これは、終了が証明できない場合でも、空のループの削除などのコンパイラ変換を許可することを目的としています。
ループの制御式は定数であるため、コンパイラはループが終了すると想定しない場合があります。これは、オペレーティング システムのように、永久に実行する必要があるリアクティブ プログラムを対象としています。
ただし、次のループの動作は不明です
a = 1; while(a);
実際、コンパイラはこのループを削除する場合と削除しない場合があり、その結果、プログラムが終了する場合と終了しない場合があります。ハードディスクを消去することは許可されていないため、実際には未定義ではありませんが、避けるべき構造です。
ただし、別の障害があります。次のコードを検討してください。
a = 1; while(a) while(1);
コンパイラは外側のループが終了すると想定する可能性があるため、内側のループも終了する必要があります。したがって、非常にスマートなコンパイラを使用している場合、while(1);
終了してはならないループには、そのような終了しないループが までずっと含まれている必要がありますmain
。volatile
本当に無限ループが必要な場合は、変数を読み書きする必要があります。
この条項が実用的でない理由
私たちのコンパイラ会社がこの節を利用する可能性はほとんどありません。これは主に、これが非常に構文的なプロパティであるためです。中間表現 (IR) では、上記の例の定数と変数の違いは、定数の伝播によって簡単に失われます。
この句の意図は、コンパイラの作成者が次のような望ましい変換を適用できるようにすることです。それほど珍しくないループを考えてみましょう:
int f(unsigned int n, int *a)
{ unsigned int i;
int s;
s = 0;
for (i = 10U; i <= n; i++)
{
s += a[i];
}
return s;
}
アーキテクチャ上の理由 (ハードウェア ループなど) から、このコードを次のように変換します。
int f(unsigned int n, int *a)
{ unsigned int i;
int s;
s = 0;
for (i = 0; i < n-9; i++)
{
s += a[i+10];
}
return s;
}
節 6.8.5 ad 6 がなければ、これは不可能n
ですUINT_MAX
。それにもかかわらず、これがこのコードの作成者の意図ではないことは、人間には明らかです。条項 6.8.5 ad 6 では、この変換が許可されています。ただし、これを実現する方法は、IR で無限ループの構文要件を維持するのが難しいため、コンパイラの作成者にとってあまり実用的ではありません。
n
およびi
がunsigned
オーバーフローするsigned int
と未定義の動作が発生するため、この理由で変換が正当化されることが不可欠であることに注意してください。ただし、効率的なコードはunsigned
、より大きな正の範囲を除けば、 を使用することでメリットがあります。
別のアプローチ
私たちのアプローチは、コードの作成者が、たとえばassert(n < UINT_MAX)
ループの前に挿入するか、または Frama-C のような保証を挿入することによって、自分の意図を表現する必要があるというものです。このようにして、コンパイラは終了を「証明」でき、6.8.5 節と 6 節に依存する必要はありません。
PS: 私は 2011 年 4 月 12 日のドラフトを見ています。paxdiablo は明らかに別のバージョンを見ているので、彼のバージョンの方が新しいかもしれません。彼の引用では、定数式の要素は言及されていません。
最も簡単な答えは、§5.1.2.3p6 からの引用です。これは、準拠する実装の最小要件を示しています。
準拠する実装の最小要件は次のとおりです。
— volatile オブジェクトへのアクセスは、抽象マシンのルールに従って厳密に評価されます。
— プログラムの終了時に、ファイルに書き込まれるすべてのデータは、抽象セマンティクスに従ってプログラムを実行した場合に生成される結果と同一でなければなりません。
— 対話型デバイスの入力および出力ダイナミクスは、7.21.3 で指定されているとおりに行われるものとします。これらの要件の意図は、プログラムが入力を待機する前にプロンプト メッセージが実際に表示されるようにするために、バッファなしまたは行バッファ付きの出力をできるだけ早く表示することです。
これは、プログラムの観察可能な動作です。
実行された最適化が原因でマシンコードが観察可能な動作を生成できない場合、そのコンパイラは C コンパイラではありません。このような無限ループのみを含むプログラムの、終了時点での観察可能な動作は何ですか? このようなループを終了させる唯一の方法は、途中で終了させるシグナルによるものです。の場合SIGTERM
、プログラムは終了します。これにより、観察可能な動作は発生しません。したがって、そのプログラムの唯一の有効な最適化は、コンパイラがシステムを横取りしてプログラムを閉じ、すぐに終了するプログラムを生成することです。
/* unoptimised version */
int main() {
for (;;);
puts("The loop has ended");
}
/* optimised version */
int main() { }
1 つの可能性として、シグナルが発生し、実行が別の場所にジャンプするように longjmp が呼び出されます。ジャンプできる唯一の場所は、ループの前の実行中にどこかに到達したようです。そのため、コンパイラがシグナルが発生して実行が別の場所にジャンプすることを認識するのに十分なほどインテリジェントである場合、ループを潜在的に最適化できます。 (そして信号を上げて)すぐにジャンプすることを支持して離れてください。
複数のスレッドが式に入る場合、有効な実装では、プログラムの所有権をメイン スレッドから別のスレッドに転送し、メイン スレッドを終了できる場合があります。最適化に関係なく、プログラムの観察可能な動作は引き続き観察可能でなければなりません。
次のステートメントが に表示されC11 6.8.5 Iteration statements /6
ます。
制御式が定数式ではなく、入出力操作を実行せず、揮発性オブジェクトにアクセスせず、その本体、制御式、または (for ステートメントの場合) で同期またはアトミック操作を実行しない反復ステートメントその式-3 は、実装によって終了すると見なされる場合があります。
定数式をwhile(1);
使用しているため、実装はそれが終了すると想定することはできません。
コンパイラは、式が非定数であり、他のすべての条件が同様に満たされている場合、そのようなループを完全に削除できます (ループが終了することを最終的に証明できない場合でも)。