web-dev-qa-db-ja.com

なぜx = x ++が未定義なのですか?

シーケンスポイント間でxを2回変更するため、未定義です。標準では、これは未定義であるため、未定義です。
それだけは知っています。

しかし、なぜ?

私の理解では、これを禁止することで、コンパイラーはより最適化できます。これは、Cが発明されたときに理にかなっている可能性がありますが、今では弱い議論のようです。
今日Cを再発明する場合、この方法でそれを行うのでしょうか、それとももっとうまくできるのでしょうか?
または、より深い問題があり、そのような式に対して一貫したルールを定義することが困難になる可能性があるため、それらを禁止するのが最善ですか?

それでは、今日Cを再発明するとします。 _x=x++_のような式の単純なルールを提案したいと思います。これは、既存のルールよりもうまく機能しているようです。
提案されたルールと既存のルールまたは他の提案と比較して、あなたの意見を聞きたいのですが。

推奨ルール:

  1. シーケンスポイント間では、評価の順序は指定されていません。
  2. 副作用はすぐに起こります。

未定義の動作はありません。式はこの値またはその値に評価されますが、確かにハードディスクをフォーマットしません(奇妙なことに、_x=x++_がハードディスクをフォーマットする実装を見たことがありません)。

式の例

  1. _x=x++_-明確に定義されており、xは変更されません。
    最初に、xがインクリメントされ(_x++_が評価されるとすぐに)、それから古い値がxに格納されます。

  2. _x++ + ++x_-xを2回インクリメントし、_2*x+2_に評価します。
    どちらの側が最初に評価されても、結果はx + (x+2)(左側が最初)または_(x+1) + (x+1)_(右側が最初)のいずれかです。

  3. x = x + (x=3)-未指定、xは_x+3_または_6_のいずれかに設定されます。
    右側が最初に評価される場合、それは_x+3_です。 _x=3_が最初に評価される可能性もあるため、それは_3+3_です。どちらの場合でも、_x=3_割り当ては、_x=3_が評価されるとすぐに発生するため、格納された値は他の割り当てによって上書きされます。

  4. x+=(x=3)-明確に定義されており、xを6に設定します。
    これは、上の式の省略形であると主張できます。
    しかし、_+=_は_x=3_の後に実行する必要があり、2つの部分では実行しないでください(xを読み取り、_x=3_を評価し、追加して保存します)新しい値)。

利点は何ですか?

いくつかのコメントはこの良い点を上げました。
_x=x++_などの式を通常のコードで使用する必要はないと思います。
実際、私はそれよりもはるかに厳格です-_x++_を_x++;_だけで使用する場合の唯一の優れた使用法だと思います。

ただし、言語のルールはできるだけシンプルにする必要があると思います。そうでなければ、プログラマはそれらを理解しません。シーケンスポイント間で変数を2回変更することを禁止するルールは、確かにほとんどのプログラマが理解していないルールです。

非常に基本的なルールはこれです:
Aが有効で、Bが有効で、それらが有効な方法で結合されている場合、結果は有効です。
xは有効なL値、_x++_は有効な式、_=_はL値と式を組み合わせる有効な方法です。来る_x=x++_は違法ですか?
C標準はここで例外を作成し、この例外はルールを複雑にします。 stackoverflow.com を検索して、この例外がどの程度ユーザーを混乱させるかを確認できます。
だから私は言う-この混乱を取り除く。

===回答の要約===

  1. なぜそれをするのですか?
    上記のセクションで説明しようとしました-Cのルールを単純にしたいと思います。

  2. 最適化の可能性:
    これはコンパイラからある程度の自由を必要としますが、それが重要であると確信するものは何も見ませんでした。
    ほとんどの最適化はまだ実行できます。たとえば、標準で順序が指定されている場合でも、_a=3;b=5;_は順序を変更できます。 _a=b[i++]_などの式も同様に最適化できます。

  3. 既存の規格を変更することはできません。
    認めます、できません。私は実際に進んで標準やコンパイラを変更できるとは思いもしませんでした。私は、物事が別の方法で行われているかどうかを考えたかっただけです。

20
ugoren

なぜそれを定義する必要があるのか​​という質問に最初に答えるべきでしょうか?追加の副作用を伴う式を許可することにより、プログラミングスタイル、可読性、保守性、またはパフォーマンスに利点はありますか?です

y = x++ + ++x;

より読みやすい

y = 2*x + 2;
x += 2;

このような変更は非常に根本的なものであり、既存のコードベースを破壊するものです。

24
Secure

この未定義の動作を行うことでより良い最適化が可能になるという主張は、今日ではnotは弱いものです。実際、今日はCが新しいときよりもはるかにstrongerです。

Cが新しいとき、これを利用して最適化を改善できるマシンは、ほとんどが理論モデルでした。他の命令と並行して実行できる/実行すべき命令についてコンパイラがCPUに指示するCPUを構築する可能性について人々は話しました。彼らは、これが未定義の振る舞いを持つことを許可することは、そのようなCPUで実際に存在する場合、命令の「インクリメント」部分を残りの命令ストリームと並行して実行するようにスケジュールできることを指摘しました。彼らは理論については正しかったが、当時、この可能性を実際に利用できるハードウェアはほとんどなかった。

それはもはや理論的なものではありません。現在、ハードウェアが生産され、広く使用されています(Itanium、VLIW DSPなど)本当にこれを利用できます。本当にdoを使用すると、コンパイラは、命令X、Y、Zをすべて並列に実行できることを指定する命令ストリームを生成できます。これはもはや理論的なモデルではありません-実際の作業を行うために実際に使用されている実際のハードウェアです。

IMO、この定義された動作を作成することは、問題の最悪可能な「解決策」に近いものです。このような表現は使用しないでください。コードの大多数にとって、理想的な動作は、コンパイラがそのような式を完全に拒否することです。当時、Cコンパイラはそれを確実に検出するために必要なフロー分析を行いませんでした。元のC標準の時点でさえ、まだ一般的ではありませんでした。

今日のコミュニティーでもそれが受け入れられるかどうかはわかりません。多くのコンパイラーはこの種のフロー分析を実行できますが、通常、最適化を要求した場合にのみ実行します。ほとんどのプログラマーは、(正気である)そもそも書けないコードを拒否できるようにするためだけに、「デバッグ」ビルドをスローダウンさせるという考えを望んでいるのではないでしょうか。

Cがやったことは、やや合理的な2番目に良い選択です。そうしないように人々に伝え、コンパイラがコードを拒否できるようにします(ただし必須ではありません)。これにより、それを使用したことがない人のためにコンパイルが遅くなるのを(さらに)回避できますが、必要に応じてそのようなコードを拒否するコンパイラを作成できます(および/または使用を選択できるフラグを拒否するフラグを設定できます)。彼らが合うと思うかどうか)。

少なくともIMOで、この定義された動作を行うことは最悪の決定である可能性があります(少なくともそれに近い)。 VLIWスタイルのハードウェアでは、インクリメントオペレーターの合理的な使用のために低速なコードを生成することを選択します。これは、コードを悪用する無意味なコードのために行うか、または処理していないことを証明するために常に広範なフロー分析を必要とするためです。安っぽいコードなので、本当に必要な場合にのみ遅い(シリアル化された)コードを生成できます。

結論:この問題を解決したい場合は、反対方向に考えるべきです。そのようなコードが何をするかを定義するのではなく、そのような式がまったく許可されないように言語を定義する必要があります(そして、ほとんどのプログラマーはおそらくその要件を強制するよりも高速なコンパイルを選択するという事実に耐えます)。

20
Jerry Coffin

C#コンパイラチームのプリンシパルデザイナーであるEric Lippertがブログに投稿しました 記事 言語仕様レベルで機能を未定義にするための選択に関するいくつかの考慮事項について。明らかに、C#は異なる言語であり、その言語設計にはさまざまな要素が含まれていますが、それでも彼の主張は関連しています。

特に、既存の実装があり、委員会の代表者もいる言語用の既存のコンパイラーの問題を指摘します。これが当てはまるかどうかはわかりませんが、ほとんどのCおよびC++関連の仕様の議論に関連する傾向があります。

また、ご指摘のとおり、コンパイラーの最適化によるパフォーマンスの可能性も注目に値します。最近のCPUのパフォーマンスはCが若いときよりも数桁高いことは事実ですが、最近行われた大量のCプログラミングは、潜在的なパフォーマンスの向上と、(仮想的な未来)CPU命令の最適化とマルチコア処理の最適化は、副作用とシーケンスポイントを処理するためのルールの制限が厳しすぎるため、馬鹿げているでしょう。

9
Tanzelax

まず、未定義の動作の definition を見てみましょう。

3.4.3

未定義の動作
動作は、この国際規格が要件を課さない、移植できないか誤ったプログラム構造または誤ったデータを使用した場合

[。翻訳または実行(診断メッセージの発行を伴う)。

3例未定義の動作の例は、整数オーバーフローでの動作です

つまり、「未定義の動作」とは、コンパイラが自由に状況を自由に処理できることを意味し、そのようなアクションはすべて「正しい」と見なされます。

議論中の問題の根本は次の節です。

6.5式

...
3演算子とオペランドのグループ化は構文で示されます。 74) 後で指定される場合を除き(関数呼び出し_()_、_&&_、_||_、_?:_、およびコンマ演算子の場合)、 部分式の評価の順序と副作用が発生する順序はどちらも指定されていません

強調が追加されました。

次のような式を考える

_x = a++ * --b / (c + ++d);
_

部分式_a++_、_--b_、c、および_++d_は、任意の順序で評価できます。さらに、_a++_が_--b_の前に評価されている場合でも、_++d_、_a++_、および_--b_の副作用は、次のシーケンスポイント(IOW)の前の任意のポイントに適用できます。 SOMECODE)__、_--b_が評価される前にapdatedになるとは限りません。他の人が言ったように、この動作の理論的根拠は、最適な方法で操作を並べ替える自由を実装に与えることです。

このため、しかし、式

_x = x++
y = i++ * i++
a[i] = i++
*p++ = -*p    // this one bit me just yesterday
_

など、異なる実装で異なる結果が生成されます(または、異なる実装での同じ実装、または周囲のコードなどに基づく)。

動作は残されていますndefinedであるため、コンパイラは「正しいことを行う」義務はありません。上記のケースは簡単にキャッチできますが、コンパイル時にキャッチするのが困難から不可能まで、かなりの数のケースがあります。

明らかに、あなたはcan評価の順序と副作用が適用される順序が厳密に定義されるように言語を設計し、JavaとC#の両方が主にそうするCおよびC++定義がもたらす問題を回避するため。

それでは、なぜ3回の標準改訂後にCにこの変更が加えられていないのですか?まず第一に、40年分の価値のあるレガシーCコードが世に出ており、そのような変更によってそのコードが壊れないという保証はありません。このような変更を行うと、既存のすべてのコンパイラーがすぐに非準拠になるため、コンパイラー作成者には少し負担がかかります。誰もが大幅な書き換えを行う必要があります。また、高速で最新のCPUであっても、評価の順序を調整することで、実際のパフォーマンスの向上を実現できます。

5
John Bode

まず、定義されていないのはx = x ++だけではないことを理解する必要があります。 x = x ++を気にする人はいません。何に定義しても意味がないからです。未定義のものは、「a = b ++でaとbが偶然同じである」に似ています。

void f(int *a, int *b) {
    *a = (*b)++;
}
int i;
f(&i, &i);

プロセッサアーキテクチャ(および例よりも複雑な関数の場合は周囲のステートメント)に最も効率的なものに応じて、関数を実装する方法がいくつかあります。たとえば、2つの明らかなもの:

load r1 = *b
copy r2 = r1
increment r1
store *b = r1
store *a = r2

または

load r1 = *b
store *a = r1
increment r1
store *b = r1

上記の最初の1つ、つまりより多くの命令とレジスタを使用するものは、aとbが異なることが証明できないすべての場合に使用する必要があるものです。

4
Random832

レガシー

Cを今日再発明できるという仮定は成り立たない。作成され、日常的に使用されているCコードの行が非常に多いため、プレイの途中でゲームのルールを変更するのは間違いです。

もちろん、あなたのルールでC + =のような新しい言語を発明することもできます。しかし、それはCではありません。

3
mouviciel

何かが定義されていると宣言しても、既存のコンパイラは変更されません。これは、多くの場所で明示的または暗黙的に依存されている可能性がある仮定の場合に特に当てはまります。

仮定の主な問題は_x = x++;_ではありません(コンパイラーは簡単に確認でき、警告する必要があります)。これは*p1 = (*p2)++および同等の(_p1[i] = p2[j]++;_(p1とp2が関数のパラメーター)_p1 == p2_(C99ではrestrictがシーケンスポイント間でp1!= p2であると仮定する可能性を広げるために追加されているかどうか)最適化の可能性が重要であったこと)。

2
AProgrammer