web-dev-qa-db-ja.com

オペレーターの優先順位と評価の順序

「演算子の優先順位」および「評価の順序」という用語は、プログラミングで非常に一般的に使用される用語であり、プログラマーが知ることは非常に重要です。そして、私が理解している限り、2つの概念は緊密に結びついています。表現について話すとき、一方は他方なしではできません。

簡単な例を見てみましょう。

_int a=1;  // Line 1
a = a++ + ++a;  // Line 2
printf("%d",a);  // Line 3
_

CおよびC++のシーケンスポイントには次のものが含まれるため、_Line 2_が未定義の動作につながることは明らかです。

  1. &&(論理積)の左オペランドと右オペランドの評価の間、|| (論理OR)、およびコンマ演算子。たとえば、式_*p++ != 0 && *q++ != 0_では、サブ式_*p++ != 0_のすべての副作用は、qにアクセスする前に完了します。

  2. 3値の「疑問符」演算子の第1オペランドの評価と、第2または第3オペランドの評価の間。たとえば、式a = (*p++) ? (*p++) : 0には、最初の_*p++_の後にシーケンスポイントがあります。これは、2番目のインスタンスが実行されるまでにすでにインクリメントされていることを意味します。

  3. 完全な表現の終わりに。このカテゴリには、式ステートメント(割り当て_a=b;_など)、returnステートメント、if、switch、while、またはdo-whileステートメントの制御式、およびforステートメントの3つの式すべてが含まれます。

  4. 関数が関数呼び出しに入力される前。引数が評価される順序は指定されていませんが、このシーケンスポイントは、関数が入力される前にすべての副作用が完了することを意味します。式f(i++) + g(j++) + h(k++)では、fは元の値iのパラメーターで呼び出されますが、iは、fの本体に入る前にインクリメントされます。同様に、jkは、それぞれghを入力する前に更新されます。ただし、f()g()h()が実行される順序や、ijkがインクリメントされる順序は指定されていません。したがって、jの本体のkfの値は未定義です。関数呼び出しf(a,b,c)は、コンマ演算子とaの評価順序を使用しないことに注意してください。 、b、およびcは指定されていません。

  5. 関数returnで、戻り値が呼び出し元のコンテキストにコピーされた後。 (このシーケンスポイントはC++標準でのみ指定されており、Cでは暗黙的にのみ存在します。)

  6. 初期化子の終了時。たとえば、宣言_int a = 5;_で5を評価した後。

したがって、ポイント#3を通過します。

完全な式の終わり。このカテゴリには、式ステートメント(割り当てa = b;など)、returnステートメント、if、switch、while、またはdo-whileステートメントの制御式、およびforステートメントの3つの式すべてが含まれます。

_Line 2_は明らかに未定義の振る舞いにつながります。これは、未定義の振る舞いシーケンスポイントとどのように緊密に結合されているかを示しています。

次に、別の例を見てみましょう。

_int x=10,y=1,z=2; // Line 4
int result = x<y<z; // Line 5
_

これで、_Line 5_によって変数resultが_1_を格納することが明らかになりました。

これで、_x<y<z_の式_Line 5_は、次のいずれかとして評価できます。

x<(y<z)または_(x<y)<z_。前者の場合、resultの値は_0_になり、後者の場合、resultは_1_になります。しかし、_Operator Precedence_が_Equal/Same_の場合、Associativityが機能するため、_(x<y)<z_として評価されます。

これはこれで言われていることです MSDN記事

C演算子の優先順位と結合性は、式のオペランドのグループ化と評価に影響します。演算子の優先順位は、優先順位が高いまたは低い他の演算子が存在する場合にのみ意味があります。優先順位の高い演算子を使用した式が最初に評価されます。優先順位は、「バインディング」という言葉でも説明できます。優先順位の高い演算子は、より緊密なバインディングを持っていると言われます。

さて、上記の記事について:

「優先順位の高い演算子を使用した式が最初に評価される」と記載されています。

正しく聞こえない場合があります。しかし、_()_も演算子_x<y<z_は_(x<y)<z_と同じであると考えると、記事は何か間違ったことを言っているとは思いません。私の推論は、結合性が機能しない場合、_<_は シーケンスポイントではないため、完全な式の評価があいまいになるということです。 -)

また、私が見つけた別のリンクは、これを 演算子の優先順位と結合性 で述べています:

このページには、C演算子が優先順位(最高から最低)の順にリストされています。それらの結合性は、式で同じ優先順位の演算子が適用される順序を示します。

したがって、_int result=x<y<z_の2番目の例を見ると、式の最も単純な形式は単一のリテラル定数またはオブジェクトで構成されているため、3つの式すべてにxy、およびzがあることがわかります。したがって、式xyzの結果は、rvalues、つまり_10_、_1_、および_2_それぞれ。したがって、_x<y<z_を_10<1<2_として解釈できるようになりました。

_10<1_または_1<2_の2つの式が評価され、演算子の優先順位が同じであるため評価されるため、結合性は機能しません。左から右へ

この最後の例を私の議論として取り上げます。

_int myval = ( printf("Operator\n"), printf("Precedence\n"), printf("vs\n"),
printf("Order of Evaluation\n") );
_

上記の例では、comma演算子の優先順位が同じであるため、式は_left-to-right_で評価され、最後のprintf()の戻り値はmyvalに格納されます。

SO/IEC 9899:201x underJ.1 Unspecified Behaviorには、次のように記載されています。

関数呼び出し()、&&、||、?:、およびコンマ演算子(6.5)で指定されている場合を除き、部分式が評価される順序と副作用が発生する順序。

今私は知りたいのですが、言うのは間違っているでしょうか:

評価の順序は演算子の優先順位に依存し、不特定の動作の場合を残します。

質問で言ったことに間違いがあった場合は訂正したいと思います。私がこの質問を投稿した理由は、MSDNの記事によって頭に浮かんだ混乱のためです。 エラーにあるかどうか?

47
Sadique

はい、MSDNの記事は、少なくとも標準のCおよびC++に関しては誤りです。1

そうは言っても、用語についてのメモから始めましょう。C++標準では、オペランドの評価を指すのに「評価」を使用し、参照するのに「値の計算」を使用します(ほとんどの場合、いくつかのスリップアップがあります)。操作を実行します。したがって、(たとえば)a + bを実行すると、abのそれぞれが評価され、値の計算が実行されて結果が決定されます。

値の計算の順序が(ほとんど)優先順位と結合性によって制御されることは明らかです。値の計算を制御することは、基本的に、優先順位と結合性の定義ですare。この回答の残りの部分では、「評価」を使用して、値の計算ではなく、オペランドの評価を参照しています。

さて、優先順位によって決定される評価順序に関しては、そうではありません!それはそれと同じくらい簡単です。たとえば、x<y<zの例を考えてみましょう。結合規則によれば、これは(x<y)<zとして解析されます。ここで、スタックマシンでこの式を評価することを検討してください。このようなことをすることは完全に許容されます:

 Push(z);    // Evaluates its argument and pushes value on stack
 Push(y);
 Push(x);
 test_less();  // compares TOS to TOS(1), pushes result on stack
 test_less();

これは、zまたはxの前にyを評価しますが、それでも(x<y)を評価し、その比較の結果をzと比較します。

概要:評価の順序は、結合性とは無関係です。

優先順位も同じです。式をx*y+zに変更しても、zまたはxの前にyを評価できます。

Push(z);
Push(y);
Push(x);
mul();
add();

概要:評価の順序は優先順位とは無関係です。

副作用を追加しても、これは同じままです。副作用を、次のシーケンスポイント(式の終わりなど)にjoinを付けて、別の実行スレッドによって実行されると考えるのは教育的だと思います。したがって、a=b++ + ++c;のようなものは次のように実行できます。

Push(a);
Push(b);
Push(c+1);
side_effects_thread.queue(inc, b);
side_effects_thread.queue(inc, c);
add();
assign();
join(side_effects_thread);

これは、明らかな依存関係が必ずしも評価の順序にも影響しない理由も示しています。 aが割り当てのターゲットですが、これはabeforebまたはcのいずれかを評価します。また、上記で「スレッド」と記述しましたが、これはスレッドのpoolである可能性もあり、すべてが並行して実行されるため、順序についての保証はありません。ある増分と別の増分のどちらかです。

ハードウェアがスレッドセーフなキューイングを直接(そしてcheap)サポートしていない限り、これは実際の実装ではおそらく使用されません(それでもそうなる可能性はほとんどありません)。スレッドセーフなキューに何かを入れると、通常、単一の増分を行うよりもかなり多くのオーバーヘッドが発生するため、実際にこれを行う人を想像するのは困難です。ただし、概念的には、このアイデアは標準の要件に適合しています。プリ/ポストインクリメント/デクリメント操作を使用する場合、式のその部分が評価された後に発生し、で完了する操作を指定します。次のシーケンスポイント。

編集:正確にはスレッド化されていませんが、一部のアーキテクチャではそのような並列実行が許可されています。いくつかの例として、Intel Itaniumおよび一部のDSPなどのVLIWプロセッサを使用すると、コンパイラは、並列で実行されるいくつかの命令を指定できます。ほとんどのVLIWマシンには、並行して実行される命令の数を制限する特定の命令「パケット」サイズがあります。 Itaniumも命令のパケットを使用しますが、現在のパケットの命令を次のパケットの命令と並行して実行できることを示すために、命令パケットのビットを指定します。このようなメカニズムを使用すると、私たちのほとんどが慣れ親しんでいるアーキテクチャで複数のスレッドを使用した場合と同じように、命令を並行して実行できます。

概要:評価の順序は、明らかな依存関係とは無関係です

次のシーケンスポイントの前に値を使用しようとすると、未定義の動作が発生します。特に、「他のスレッド」はその間にそのデータを(潜在的に)変更しており、アクセスを同期する方法がありますno他のスレッドで。それを使おうとすると、未定義の動作につながります。

(確かに、今ではかなり遠い)例として、64ビット仮想マシンで実行されているコードを考えてみてください。実際のハードウェアは8ビットプロセッサです。 64ビット変数をインクリメントすると、次のようなシーケンスが実行されます。

load variable[0]
increment
store variable[0]
for (int i=1; i<8; i++) {
    load variable[i]
    add_with_carry 0
    store variable[i]
}

そのシーケンスの途中で値を読み取ると、一部のバイトのみが変更されたものを取得できるため、取得するのは古い値nor新しい値ではありません。

この正確な例はかなり遠いものかもしれませんが、それほど極端ではないバージョン(たとえば、32ビットマシンの64ビット変数)は実際にはかなり一般的です。

結論

評価の順序はnot優先順位、結合性、または(必然的に)明らかな依存関係に依存します。式の他の部分で前後のインクリメント/デクリメントが適用されている変数を使用しようとすると、実際にはcompletely未定義の動作が発生します。実際にクラッシュする可能性は低いですが、古い値または新しい値のいずれかを取得することが確実にnot保証されています-完全に別のものを取得する可能性があります。


1 私はこの特定の記事をチェックしていませんが、かなりの数のMSDN記事がMicrosoftのマネージC++および/またはC++/CLI(またはC++の実装に固有)について説明していますが、それらが適用されないことを指摘することはほとんどまたはまったくありません標準のCまたはC++に。これは、彼らが彼ら自身の言語に適用することを決定した規則が実際に標準言語に適用されると主張しているという誤った外観を与える可能性があります。このような場合、記事は技術的に誤りではありません。標準のCまたはC++とは何の関係もありません。これらのステートメントを標準のCまたはC++に適用しようとすると、結果はfalseになります。

42
Jerry Coffin

優先順位が評価の順序に影響を与える唯一の方法は、依存関係を作成することです。それ以外の場合、2つは直交しています。優先順位によって作成された依存関係が評価の順序を完全に定義するという些細な例を注意深く選択しましたが、これは一般的には当てはまりません。また、多くの式には2つの効果があることも忘れないでください。1つは値になり、もう1つは副作用があります。これら2つを一緒に発生させる必要はないため、依存関係によって特定の評価順序が強制される場合でも、これは値の評価順序のみです。副作用には影響しません。

12
James Kanze

これを確認する良い方法は、式ツリーを取得することです。

式がある場合、たとえば_x+y*z_とすると、それを式ツリーに書き換えることができます。

優先度と結合性のルールの適用:

_x + ( y * z )
_

優先度と結合性のルールを適用した後は、それらを安全に忘れることができます。

ツリー形式:

_  x
+
    y
  *
    z
_

これで、この式のリーフはxy、およびzになります。これは、xy、およびzを任意の順序で評価できること、および_*_の結果を評価できることを意味します。およびxを任意の順序で。

これらの式には副作用がないので、あまり気にしません。ただし、その場合、順序によって結果が変わる可能性があります。また、順序はコンパイラが決定するものであれば何でもかまいません。問題が発生します。

さて、シーケンスポイントはこの混乱に少し秩序をもたらします。彼らは効果的に木をセクションに切りました。

_x + y * z, z = 10, x + y * z_

優先順位と結合性の後

x + ( y * z ) , z = 10, x + ( y * z)

ツリー:

_      x
    +
        y
      *
        z
  , ------------
      z
    =
      10     
  , ------------
      x
    +
        y
      *
        z   
_

ツリーの上部は中央の前に評価され、中央は下部の前に評価されます。

7
Let_Me_Be

「優先順位の高い演算子を使用した式が最初に評価される」と記載されています。

私が言ったことを繰り返すつもりです ここ 。標準のCおよびC++に関する限り、記事に欠陥があります。優先順位は、各演算子のオペランドと見なされるトークンにのみ影響しますが、評価の順序にはまったく影響しません。

したがって、このリンクは、言語自体がどのように機能するかではなく、Microsoftがどのように実装したかを説明しているだけです。

4
Prasoon Saurav