Cでの(式に対する)配列インデックスの評価順序


47

このコードを見て:

static int global_var = 0;

int update_three(int val)
{
    global_var = val;
    return 3;
}

int main()
{
    int arr[5];
    arr[global_var] = update_three(2);
}

どの配列エントリが更新されますか?0または2?

この特定の場合の操作の優先順位を示すCの仕様の一部はありますか?


21
これは未定義の動作のにおいがします。それは、意図的にコード化してはならないものです。
ビット

1
私はそれが悪いコーディングの例であることに同意します。
ジミニオン

4
いくつかの逸話的な結果:godbolt.org/z/hM2Jo2
Bob__

15
これは、配列のインデックスや操作の順序とは関係ありません。これは、C仕様で「シーケンスポイント」と呼ばれるものと関係があります。特に、割り当て式では、左側の式と右側の式の間にシーケンスポイントが作成されないため、コンパイラは自由に実行できます。選択します。
リーダニエルクロッカー

4
clangこのコードが警告IMHOをトリガーするように、機能要求を報告する必要があります。
マラット

回答:


51

左と右のオペランドの順序

で割り当てを実行するにarr[global_var] = update_three(2)は、Cの実装でオペランドを評価し、副作用として、左側のオペランドの格納された値を更新する必要があります。C 2018 6.5.16(割り当てについて)のパラグラフ3は、左と右のオペランドに順序付けがないことを示しています。

オペランドの評価は順序付けされていません。

つまり、Cの実装では、最初に左辺値 を計算して(左辺値を計算arr[global_var]することで、この式の意味を理解することを意味します)、次に評価しupdate_three(2)、最後に後者の値を前者に割り当てます。または、update_three(2)最初に評価し、次に左辺値を計算してから、前者を後者に割り当てます。または、左辺値をupdate_three(2)いくつかの方法で評価してから、右辺の値を右辺の左辺値に割り当てます。

いずれの場合も、値の左辺値への割り当ては最後にする必要があります。6.5.163も次のように述べているためです。

…左のオペランドの格納された値を更新することの副作用は、左と右のオペランドの値の計算後にシーケンスされます…

シーケンス違反

一部の人はglobal_var、6.5 2に違反してそれを使用したり個別に更新したりするために、未定義の動作について熟考している可能性があります。

スカラーオブジェクトの副作用が同じスカラーオブジェクトの異なる副作用または同じスカラーオブジェクトの値を使用した値の計算に関連してシーケンスされていない場合、動作は未定義です…

などの式の動作がx + x++C規格で定義されていないことは、多くのCの専門家にはよく知られています。なぜなら、xシーケンスの値を使用することも、同じ式で個別に変更することもないためです。ただし、この場合は、シーケンスを提供する関数呼び出しがあります。global_varで使用arr[global_var]され、関数呼び出しで更新されますupdate_three(2)

6.5.2.2 10は、関数が呼び出される前にシーケンスポイントがあることを示しています。

関数指定子と実際の引数の評価の後、実際の呼び出しの前にシーケンスポイントがあります…

関数の内部にglobal_var = val;は、完全な式があり、6.8の3in もそうですreturn 3;

完全な表現は別の表現の一部、また宣言子または抽象宣言子の一部ではない表現であります...

次に、これらの2つの式の間にシーケンスポイントがあります。これも6.8 4に従います。

…完全な式の評価と、評価される次の完全な式の評価の間にシーケンスポイントがあります。

したがって、C実装はarr[global_var]最初に評価してから関数呼び出しを実行できます。その場合、関数呼び出しの前にシーケンスポイントがあるため、シーケンスポイントが存在します。または、関数呼び出しで評価global_var = val;してからarr[global_var]、完全な式の後に1つあるため、それらの間のシーケンスポイント。したがって、動作は特定されていません(これら2つのいずれかが最初に評価される可能性があります)が、未定義ではありません。


24

ここでの結果は不定です。

部分式がどのようにグループ化されるかを決定する式の演算の順序は明確に定義されていますが、評価の順序は指定されていません。この場合、global_var最初に読み取られるか、update_three最初にが呼び出される可能性がありますが、どちらかを知る方法はありません。

関数呼び出しはシーケンスポイントを導入するため、ここで未定義の動作はありませんglobal_var。変更するステートメントを含む関数内のすべてのステートメントと同様です。

明確にするために、C標準ではセクション3.4.3で未定義の動作を次のように定義しています

未定義の動作

この国際標準が要件を課さない、移植不能またはエラーのあるプログラム構造またはエラーのあるデータの使用時の動作

また、セクション3.4.4で未指定の動作を次のように定義しています。

不特定の行動

未指定の値の使用、またはこの国際規格が2つ以上の可能性を提供し、どのインスタンスで選択されるかについてさらなる要件を課さない他の動作

標準では、関数の引数の評価順序は指定されていません。つまり、この場合arr[0]、3に設定されるか、3 arr[2]に設定されます。


「関数呼び出しはシーケンスポイントを導入する」では不十分です。左のオペランドが最初に評価される場合は、シーケンスポイントが関数の評価から左のオペランドを分離するため、それで十分です。ただし、関数呼び出しの後に左オペランドが評価される場合、関数の呼び出しによるシーケンスポイントは、関数内の評価と左オペランドの評価の間にありません。完全な式を区切るシーケンスポイントも必要です。
Eric Postpischil

2
@EricPostpischil C11より前の用語では、関数の入口と出口にシーケンスポイントがあります。C11の用語では、関数本体全体が呼び出しコンテキストに関して不確定に順序付けられます。これらは両方とも同じものを指定していますが、異なる用語を使用しています
MM

これは間違いです。割り当ての引数の評価順序は指定されていません。この特定の割り当ての結果については、移植性がなく、本質的に間違っている(セマンティクスまたは意図された結果のいずれかと矛盾する)信頼性のないコンテンツを持つ配列の作成です。未定義の動作の完璧なケース。
kuroi neko

1
@kuroineko出力が変化する可能性があるからといって、動作が自動的に未定義になるわけではありません。標準には、未定義の動作と未指定の動作の定義が異なります。この状況では、後者です。
1

@EricPostpischilここにシーケンスポイントがあります(C11有益な付属文書Cから):「関数呼び出しの評価と実際の引数と実際の呼び出しの引数の間(6.5.2.2)」、「完全な式の評価の間評価される次の完全な式... /-/ ... returnステートメントの(オプションの)式(6.8.6.4) " そして、各セミコロンでも、それが完全な表現であるためです。
ランディン

1

私が試したところ、エントリ0が更新されました。

ただし、この質問によれば、常に最初に評価される式の右側が

評価の順序は不特定であり、順序付けされていません。だから私はこのようなコードは避けられるべきだと思います。


エントリ0でも更新を取得しました。
ジミニオン

1
動作は未定義ではありませんが、指定されていません。当然どちらかに依存することは避けるべきです。
Antti Haapala

編集した@AnttiHaapala
Mickael B.

1
ええと、シーケンスされていませんが、不規則にシーケンスされています...キューにランダムに立っている2人が不規則にシーケンスされています。エージェントスミス内のネオはシーケンスされておらず、未定義の動作が発生します。
Antti Haapala

0

割り当てる値を得る前に割り当てのコードを発行することはほとんど意味がないため、ほとんどのCコンパイラは最初に関数を呼び出すコードを発行し、その結果をどこかに(レジスター、スタックなど)保存してから、そのコードを発行しますこの値を最終的な宛先に書き込むため、変更後にグローバル変数を読み取ります。これを「標準」と呼び、標準ではなく純粋なロジックで定義します。

しかし、最適化の過程では、コンパイラは値を一時的にどこかに保存する中間ステップを排除し、関数の結果をできるだけ最終的な宛先に直接書き込もうとします。その場合、多くの場合、最初にインデックスを読み取る必要があります。 、たとえば、レジ​​スタに、関数の結果を配列に直接移動できるようにします。これにより、変更前にグローバル変数が読み取られる可能性があります。

したがって、これは基本的に未定義の動作であり、最適化が実行されているかどうか、およびこの最適化がどの程度積極的であるかによって結果が異なる可能性が非常に高いプロパティです。次のいずれかのコーディングによってその問題を解決するのは、開発者としてのあなたの仕事です。

int idx = global_var;
arr[idx] = update_three(2);

またはコーディング:

int temp = update_three(2);
arr[global_var] = temp;

経験則として:グローバル変数がconst変更されていない場合(または変更されていない場合は、副作用としてコードを変更しないことがわかっている場合)は、マルチスレッド環境のように、コード内で直接使用しないでください。これも未定義になる可能性があります:

int result = global_var + (2 * global_var);
// Is not guaranteed to be equal to `3 * global_var`!

コンパイラはそれを2回読み取る可能性があり、別のスレッドが2回の読み取りの間に値を変更する可能性があるためです。それでも、最適化によってコードは確実にコードを1回だけ読み取るため、別のスレッドのタイミングにも依存する異なる結果が再び得られる可能性があります。したがって、使用前にグローバル変数を一時的なスタック変数に格納すれば、頭痛が大幅に軽減されます。コンパイラがこれが安全であると考えている場合は、それでも最適化し、代わりにグローバル変数を直接使用する可能性が高いので、結局のところ、パフォーマンスやメモリ使用量に違いはありません。

(誰かがなぜx + 2 * x代わりに誰かがなぜするのかと尋ねる場合に備えて3 * x-一部のCPUでは、追加は超高速であり、コンパイラがこれらをビットシフト(2 * x == x << 1)に変換するので2のべき乗による乗算ですが、任意の数値との乗算は非常に遅くなる可能性がありますしたがって、3を乗算する代わりに、xを1だけビットシフトして結果にxを追加することで、コードが大幅に高速化されます。さらに、3を乗算して最新のターゲットでない限り、積極的な最適化をオンにすると、最新のコンパイラによってそのトリックが実行されます。乗算が加算と同じくらい高速なCPU。それ以降、トリックによって計算が遅くなります。)


2
それは未定義の動作ではありません-標準は可能性をリストし、それらのいずれかが任意のインスタンスで選択されます
Antti Haapala

コンパイラーは3 * x、xの2つの読み取りにはなりません。xを1回読み取ってから、xを読み込んだレジスタに対してx + 2 * xメソッドを実行する可能性があります
MM

6
@Mecki 「コードを見ただけでは結果が言えない場合、結果は未定義です」 - 未定義の動作は、C / C ++で非常に特定の意味を持ち、それではありません。他の回答者は、この特定のインスタンスが指定されていないが、未定義でない理由を説明しています。
marcelm

3
元の質問の範囲を超えていても、コンピュータの内部に光を当てる意図に感謝します。ただし、UBは非常に正確なC / C ++専門用語であり、特に質問が言語の専門性に関する場合は、慎重に使用する必要があります。代わりに適切な「指定されていない動作」という用語を使用することを検討してください。
kuroi neko

2
@Mecki「Undefinedは英語で非常に特別な意味を持っています」...しかし、のラベルが付いた質問では、問題language-lawyerの言語がundefinedに対して独自の「非常に特別な意味」を持っているので、言語の定義。
トライプハウンド

-1

グローバル編集:ごめんなさい、みんな元気になって、多くのナンセンスを書いてしまいました。ただの古いギーザが怒鳴っています。

Cは免れたと思っていましたが、C11以降、C ++に匹敵します。どうやら、コンパイラーが式の副作用で何をするかを知るには、「同期点の前にある」に基づくコードシーケンスの部分的な順序付けを含む小さな数学の謎を解く必要があります。

私はたまたまK&R時代にいくつかの重要なリアルタイム組み込みシステムを設計および実装しました(エンジンがチェックされない場合に人々を最も近い壁に衝突させる可能性がある電気自動車のコントローラー、10トンの産業用など)適切に命令されない場合、人々をパルプに押しつぶすことができるロボット、そして無害ではあるが、数十のプロセッサが1%未満のシステムオーバーヘッドでデータバスを乾燥させるシステムレイヤー)。

未定義と未指定の違いを理解するには、私は老朽化しているか愚かすぎるかもしれませんが、同時実行とデータアクセスの意味についてはかなりよく理解していると思います。私の議論の余地のある意見では、C ++へのこだわりと、今やペットの言語が同期の問題を引き継ぐCの執着は、コストのかかるパイプの夢です。同時実行が何であるかを知っていて、これらのギズモが必要ない場合、または必要ない場合、それを台無しにしようとせずに、世界全体で好意を示します。

この目を見張るようなメモリバリア抽象化のトラックロードはすべて、マルチCPUキャッシュシステムの一時的な一連の制限によるものです。これらはすべて、たとえばミューテックスや条件変数C ++などの一般的なOS同期オブジェクトに安全にカプセル化できます。オファー。
このカプセル化のコストはわずかですが、特定の細かいCPU命令を使用することで達成できるものと比較して、パフォーマンスがわずかに低下する場合があります。キーワード(またはA
volatile#pragma dont-mess-with-that-variableすべての私にとって、システムプログラマとしての注意)は、コンパイラにメモリアクセスの並べ替えを停止するように指示するには十分だったでしょう。直接的なasmディレクティブを使用して最適なコードを簡単に生成し、低レベルのドライバーとOSコードにアドホックCPU固有の命令を振りかけることができます。基盤となるハードウェア(キャッシュシステムまたはバスインターフェイス)がどのように機能するかについての深い知識がなければ、とにかく役に立たない、非効率的な、または欠陥のあるコードを書くことになります。

volatileキーワードとボブの微調整は、誰もが最もハードボイルドされた低レベルのプログラマーのおじでした。その代わりに、C ++の数学のフリークの通常のギャングは、実在しない問題を探すソリューションを設計し、コンパイラーの仕様とプログラミング言語の定義を誤解するという典型的な傾向に屈し、さらに別の不可解な抽象化を設計しました。

これらの「バリア」は、低レベルのCコードでも適切に機能するように生成する必要があったため、今回の変更だけでCの基本的な側面を改ざんする必要がありました。それは、とりわけ、表現の定義に大きな混乱をもたらし、説明や正当化はまったくありません。

結論として、コンパイラがこのばかげたCの部分から一貫したマシンコードを生成できるという事実は、C ++の連中が2000年代後半のキャッシュシステムの潜在的な不整合に対処した方法の遠い結果にすぎません。
これはC(式定義)の1つの基本的な側面をひどく混乱させたので、Cプログラマーの大多数-キャッシュシステムについて気にしなくて、正しくそうである-を説明するために教祖に頼らざるを得なくなった。違いa = b() + c()a = b + c

この不幸な配列がどうなるかを推測しようとすると、とにかく時間と労力の純損失になります。コンパイラがそれをどうするかに関わらず、このコードは病理学的に間違っています。それを行う唯一の責任があることは、それを箱に送ることです。
概念的には、別のステートメントで評価の前または後に変更を明示的に発生させる簡単な努力で、副作用を常に式から取り除くことができます。
コンパイラーが何かを最適化することを期待できなかったときに、この種のくだらないコードは80年代に正当化されたかもしれません。しかし、今やコンパイラーはほとんどのプログラマーよりもずっと賢くなっているので、残っているのはひどいコードの断片だけです。

私はまた、この未定義/不特定の議論の重要性を理解することができません。コンパイラに依存して、一貫した動作のコードを生成することも、そうしないこともできます。あなたがそれを未定義と呼ぶか特定しないと呼ぶかは論争のように思えます。

私の議論の余地のある意見では、CはそのK&R状態ではすでに十分危険です。有用な進化は、常識的な安全対策を追加することです。たとえば、この高度なコード分析ツールを使用すると、仕様によりコンパイラーは、極端に信頼できない可能性のあるコードを静かに生成するのではなく、少なくともbonkersコードに関する警告を生成するように実装するように強制します。
しかし代わりに、たとえばC ++ 17で固定評価順序を定義することを決定しました。現在、あらゆるソフトウェアは、新しいコンパイラーが難読化を決定論的な方法で熱心に処理するという確信に基づいて、意図的に自分のコードに副作用を配置するよう積極的に扇動されています。

K&Rは、コンピューティング業界の真の驚異の1つでした。20ドルで、言語の包括的な仕様(この本だけを使用して完全なコンパイラーを書いている個人を見たことがあります)、優れたリファレンスマニュアル(目次は通常、あなたの回答の数ページ以内を示しています)質問)、そして賢明な方法で言語を使用することを教える教科書。言語を乱用して非常に非常に愚かなことをすることができる多くの方法についての理論的根拠、例、および賢明な警告の言葉を記入してください。

わずかな利益でその遺産を破壊することは、私にとって残酷な無駄のように思えます。しかし、繰り返しになりますが、私はそのポイントを完全に理解することはできません。たぶん、ある種の魂が、これらの副作用を大幅に利用する新しいCコードの例の方向に私を向けるかもしれません。


同じ式、C17 6.5 / 2の同じオブジェクトに副作用がある場合の動作は未定義です。これらは、C17 6.5.18 / 3に従ってシーケンスされていません。しかし、6.5 / 2のテキスト「スカラーオブジェクトの副作用が同じスカラーオブジェクトの異なる副作用または同じスカラーオブジェクトの値を使用する値の計算に関連してシーケンスされていない場合、動作は未定義です。」関数内の値の計算は、配列演算子のシーケンス演算子自体に関係なく、配列インデックスアクセスの前または後にシーケンスされるため、適用されません。
ランディン

関数呼び出しは、「シーケンスされていないアクセスに対するmutex」のように機能します。不明瞭なカンマ演算子のようながらくたに似ています0,expr,0
ランディン

「未定義の動作は、診断が難しい特定のプログラムエラーをキャッチしないライセンスをインプリメンターに与えます。また、準拠する言語拡張の可能性のある領域を識別します。インプリメンターは、公式には未定義の動作の定義。」また、規格は厳密に準拠していない有用なプログラムを軽視することを想定していないと述べた。標準の作成者のほとんどは、高品質のコンパイラを作成しようとしている人々が明白だと思っていたと思います...
supercat

...コンパイラを顧客にとって可能な限り有用にする機会としてUBを使用する必要があります。コンパイラの作者が「あなたのコンパイラはこのコードを他の誰よりも役に立たない」という不満に応えるための言い訳としてそれを使うとは誰も想像しなかったと思います。標準によって動作が規定されていないプログラムを効果的に処理することは、壊れたプログラムの作成を促進するだけです。」
スーパーキャット

私はあなたの発言の要点を見逃しています。コンパイラ固有の動作に依存することで、移植性が保証されます。また、いつでもこれらの「特別な定義」のいずれかを中止する可能性があるコンパイラの製造元への多大な信頼も必要です。コンパイラーができる唯一のことは警告を生成することです。警告は賢明で知識のあるプログラマーが同様のエラーを処理することを決定する場合があります。このISOモンスターに関して私が目にする問題は、OPの合法的な例のように非常に恐ろしいコードを作成することです(式のK&R定義と比較すると、非常に不明確な理由により)。
kuroi neko
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.