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


19

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

しかし、なぜ?

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

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

推奨ルール:

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

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

式の例

  1. x=x++-明確に定義され、変更されませんx
    最初に、xx++評価されるとすぐに)インクリメントされ、次に古い値がに保存されxます。

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

  3. x = x + (x=3)-指定なし、またはにx設定。 右側が最初に評価される場合、それはです。最初に評価される可能性もあるため、です。どちらの場合でも、評価は評価されるとすぐに行われるため、保存されている値は他の割り当てによって上書きされます。x+36
    x+3x=33+3x=3x=3

  4. x+=(x=3)-明確に定義され、x6 に設定されます。
    これは、上記の表現の単なる省略形であると言えます。
    ただし、2つの部分(読み取り、評価、追加、新しい値の保存)ではなく、の+=x=3に実行する必要があります。xx=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. 既存の標準を変更することはできません。
    認められない、できない。規格やコンパイラを実際に変更できるとは思っていませんでした。物事が違ったやり方でできたのかどうかを考えたかっただけです。


10
なんで、これが大事なの?定義する必要がありますか?xそれ自体に割り当てることはあまり意味がなく、増分したいx場合は、単に言うことができますx++;-割り当ての必要はありません。何が起こるかを思い出すのが難しいという理由だけで、定義すべきはないと言います。
カレブ

4
私の心では、これは良い質問です(「一部の男性は物事をあるがままに見て理由を尋ねます。私は決してなかったものを夢見、なぜそうではないかを尋ねます」)。私の意見では、これは純粋に言語設計に関する質問であり、C構文を例として使用しており、C構文に関する質問ではありません。個人的には、x ++ + ++ xやx = x ++などの式の動作を定義していない理由は、それらが誤読される可能性があるからだと考えています。
ジェイミーテイラー

5
@ugoren:結果を予測する必要があるのはなぜですか。あなたがこのようなコードを書いたとしても、それが最初のコードレビューで拒否されたとしても、彼らの正しい心の誰もそのようなコードを書かないでしょう(何度も言及されているように)。そのため、動作を定義し、オプティマイザーに最適化の最適な機会を与える必要はありません。あなたが提案するすべての例で、もし彼らがそれをコードベースに追加したら誰かを撃つだろう。
マーティンヨーク

3
これを書くのはなぜエラーではないのかというより興味深い質問を見つけるでしょうか?確かにコンパイラは未定義の振る舞いを検出できるため、ユーザーが実際に望んでいたものではない可能性があるので、エラーにならないのはなぜですか?私が検出する未定義bahavourにくいのいくつかの事例が、このISNを理解;トン
JohnB

3
シーケンスポイント間で変数を2回変更することを禁止するルールは、ほとんどのプログラマーが理解していないルールです。」-これについての証拠はありますか?私が見たほとんどの質問は、プログラマーがルールを知らなかったために尋ねられました。それが説明された後、彼らのほとんどがまだそれを理解しなかったという証拠がありますか?
セキュリティで保護する

回答:


24

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

y = x++ + ++x;

より読みやすい

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

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


1
質問に「理由」セクションを追加しました。これらの表現を使用することはお勧めしませんが、表現の意味を伝えるための簡単なルールを作成することに興味があります。
ウゴレン

また、未定義の動作を呼び出さない限り、この変更は既存のコードを壊しません。私が間違っている場合は修正してください。
-ugoren

3
さて、もっと哲学的な答え:それは現在未定義です。プログラマーがそれを使用しない場合、コードはないはずなので、そのような式を理解する必要はありません。それらを理解する必要がある場合は、明らかに、未定義の動作に依存する多くのコードが存在する必要があります。;)
確保

1
定義上、動作を定義するために既存のコードベースを壊すことはありません。それらにUBが含まれていた場合、それらは定義上、すでに壊れていました。
DeadMG

1
@ugoren:あなたの「なぜ」セクションは実際的な質問にまだ答えていません:なぜあなたはあなたのコードでこの奇妙な表現をしたいのですか?それに対する納得のいく答えが思いつかない場合、議論全体は無意味です。
マイクバランザック

20

この未定義の動作を行うことで最適化が改善されるという議論は、今日で弱くありません。実際、Cが新しくなったときよりも今日はずっと強いです。

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

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

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

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

Cがやったことは、準合理的な次善の選択です。人々にそうしないように伝え、コンパイラがコードを拒否することを許可します(必須ではありません)。これにより、それを使用したことがない人のコンパイルが(さらに)遅くなることはありませんが、誰かがそのようなコードを拒否するコンパイラを書くことができます(そして/または人々がそれを拒否するフラグを使用することを選択できます)彼らが合うと思うかどうか)。

少なくともIMOで、この定義された動作を行うことは、最悪の決定を下すことです(少なくともそれに近い)。VLIWスタイルのハードウェアでは、増分演算子を乱用する安っぽいコードのためだけに、増分演算子を適切に使用するために低速のコードを生成するか、対処していないことを証明するために常に広範なフロー分析が必要になります安っぽいコードなので、本当に必要な場合にのみ低速(シリアル化)コードを生成できます。

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


IMO、ほとんどの場合、遅い命令は実際には速い命令よりもはるかに遅く、これらは常にプログラムのパフォーマンスに影響を与えると信じる理由はほとんどありません。これを時期尚早な最適化の下に分類します。
DeadMG

たぶん私は何かが欠けている-誰もそのようなコードを書くことになっていないなら、なぜそれを最適化することに気を配るのか?
ウゴレン

1
@ugoren:(a=b[i++];たとえば)のようなコードを書くのは問題ありません。最適化するのは良いことです。しかし、そのようなもの++i++が定義された意味を持つように、そのような合理的なコードを傷つけるという点はわかりません。
ジェリーコフィン

2
@ugoren問題は診断の1つです。のような式を完全に禁止しない唯一の目的は++i++、一般に、副作用のある有効な式と区別するのが難しいことです(などa=b[i++])。Dragon Bookを正しく覚えていれば、それは実際にはNP困難な問題です。そのため、この動作は禁止ではなくUBです。
コンラッドルドルフ

1
パフォーマンスが有効な議論であるとは思わない。どちらの場合も非常にわずかな違いと非常に高速な実行を考慮して、ケースが十分に一般的であると信じるのに苦労しています。多くのプロセッサとアーキテクチャでは、事実上無料であることは言うまでもありません。
-DeadMG

9

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

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

また、あなたが言ったように、コンパイラ最適化のパフォーマンスの可能性も注目に値します。最近のCPUのパフォーマンスは、Cが若い頃よりも桁違いに大きいことは事実ですが、最近行われた大量のCプログラミングは、潜在的なパフォーマンスの向上と(仮想的な未来)CPU命令の最適化とマルチコア処理の最適化は、副作用とシーケンスポイントを処理するための一連の規則が過度に制限されているため、除外するのはばかげています。


リンク先の記事から、C#は私が提案するものからそれほど遠くないように思えます。副作用の順序は、「副作用を引き起こすスレッドから観察した場合」と定義されています。マルチスレッドについては言及しませんでしたが、一般に、Cは別のスレッドのオブザーバーに対してあまり保証しません。
ウゴレン

5

最初に、未定義の動作の定義を見てみましょう。

3.4.3

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

2環境に固有の文書化された方法(診断メッセージの発行ありまたはなし)、翻訳または実行の終了(診断メッセージの発行あり)。

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

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

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

6.5式

...
3演算子とオペランドのグループ化は構文によって示されます。 74) 特異的に(後編ファンクションコールのためされている場合を除き()&&||?:)、およびコンマ演算子、部分式の評価の順序及び副作用が起こる順序は、両方unspeciのfi EDあります

強調が追加されました。

次のような式が与えられた場合

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

部分式はa++--bc、と++d評価することができる任意の順序で。さらに、、、およびの副作用はa++、次のシーケンスポイントの前の任意のポイントに適用できます(IOWは、以前に評価された場合でも、評価される前に更新さ れることは保証されません)。他の人が言ったように、この動作の理論的根拠は、実装に最適な方法で操作を並べ替える自由を与えることです。 --b++da++--ba--b

ただし、このため、次のような式

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

などにより、実装ごとに異なる結果が得られます(または、最適化設定が異なる同じ実装、または周囲のコードなどに基づいて)。

動作は未定義のままであるため、コンパイラは、それが何であれ「正しいことをする」義務を負いません。上記のケースはキャッチするのに十分簡単ですが、コンパイル時にキャッチするのが困難または不可能になるケースが非常に多くあります。

明らかに、評価の順序と副作用が適用される順序が厳密に定義されるように言語設計できます。JavaとC#の両方は、CとC ++の定義が引き起こす問題を回避するために、JavaとC#の両方が厳密に定義されます。

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


1
問題の非常に良い説明。私はレガシーアプリケーションを壊すことに同意しません-未定義/未指定の動作が実装される方法は、標準を変更せずにコンパイラのバージョン間で変わることがあります。定義済みの動作を変更することはお勧めしません。
ウゴレン

4

最初に、定義されていないのは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

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


あなたは私の提案がより多くの機械操作をもたらす事例を実際に示していますが、それは私には取るに足らないように見えます。そして、コンパイラにはまだある程度の自由があります-私が追加する唯一の本当の要件は、b前に保存することaです。
ウゴレン

3

レガシー

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

もちろん、ルールを使用してC + =などの新しい言語を発明できます。しかし、それはCではありません。


2
今日、Cを再発明できるとは思いません。これは、これらの問題について議論できないという意味ではありません。ただし、私が提案するのは、実際に再発明することではありません。未定義の動作を定義済みまたは未指定に変換することは、標準を更新するときに行うことができ、言語はまだCです
。– ugoren

2

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

仮定の主な問題はx = x++;(コンパイラーが簡単にチェックでき、警告する必要がある)ではなく、コンパイラーが(C99で)簡単に知ることができない*p1 = (*p2)++p1[i] = p2[j]++;p1とp2が関数のパラメーターである場合)と同等ですシーケンスポイント間でp1!= p2を想定する可能性を広げるために追加されたため、最適化の可能性が重要であると見なされました)。p1 == p2restrict


私の提案がどのように変化するかわかりませんp1[i]=p2[j]++。コンパイラがエイリアシングを想定できない場合、問題はありません。それができない場合、それは本で行く必要があります- p2[j]最初にインクリメントし、p1[i]後で保存します。重要ではないと思われる最適化の機会が失われることを除いて、問題は見当たりません。
ウゴレン

2番目の段落は最初の段落から独立していませんでしたが、仮定が入り込んで追跡が困難な場所の例です。
AProgrammer

最初の段落では、非常に明白なことを述べています。新しい標準に準拠するには、コンパイラを変更する必要があります。私はこれを標準化して、コンパイラの作者に追随させる機会は本当にないと思います。議論する価値があると思う。
ウゴレン

問題は、それを必要とする言語の変更についてコンパイラを変更する必要があるということではなく、変更が広範であり、見つけにくい場所にあるということです。最も実用的なアプローチは、おそらくオプティマイザーが動作する中間形式を変更することです。つまり、x = x++;記述されていないふりをしたり、セマンティックとして必要なものをt = x; x++; x = t;使用しx=x; x++;たりします(診断についてはどうですか?)。新しい言語の場合は、副作用を取り除いてください。
AProgrammer

コンパイラの構造についてあまり知りません。すべてのコンパイラを本当に変更したい場合は、もっと気になります。しかしx++、それが関数呼び出しであるかのように、シーケンスポイントとして扱うこともinc_and_return_old(&x)できます。
ウゴレン

-1

場合によっては、この種のコード新しいC ++ 11標準で定義されています。


5
手入れをしますか?
ウゴレン

x = ++x今では明確に定義されていると思われます(しかしそうではありませんx = x++
MM
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.