39148


「基礎から学ぶC++の教科書」という本で

> 大部分の演算において、被演算子の評価順序は決まっていません。  
>  たとえば、式A+式Bという演算があったときに、式Aと式Bのどちらが先に評価されるかは決まっていません。  
>  ですから、評価の順序に依存するようなコードを書かないように注意してください。次のコードの実行結果は、おそらく納得できるものにはならないでしょう。
[code]

      int n = 1;
      n = --n / --n;
      cout << n << endl;
    
[/code]

と書いてありました。  
これを実際に実行してみると1と表示され、確かに予想していなかった結果でした。

ただ、これは評価順序が不定だからという理由では納得できません。

何故なら左から評価すれば、

[code]

    n=0/-1
    
[/code]

なので、0。

右から評価すれば

[code]

    n=-1/0
    
[/code]

で0で除算する事になりエラーとなり、  
評価順序がどちらの場合も1にはならないように思えるからです。

何故1になるのでしょうか？

「同じ数/同じ数」という形で評価されて1となっていると仮定すると、  
同時に評価されていると考える事できますが、  
そのような事はあるのでしょうか？


CC BY-SA 3.0
2017-10-30T05:28:05.613
39144
2017-10-30T09:45:00.127
25791
question
c++
被演算子の評価順序について


> これは評価順序が不定だからという理由では納得できません。

直接原因は、式`n = --n / --n;`の評価順序が不定なことにより「未定義の動作(Undefined Behavior)」が生じるためです。

プログラミング言語CやC++では「未定義の動作」に対して、何の保証も与えなくて良いという仕様になっています。質問中の例で言えば、値`0`となることもゼロ除算となることも、値`1`となることも、全てがこの式の実行に対する正しい結末と言えます。（もちろん、プログラマ視点では納得いかないでしょうけど...）

* * *

> 「同じ数/同じ数」という形で評価されて1となっていると仮定すると、同時に評価されていると考える事できますが、そのような事はあるのでしょうか？

ありえます。C++コンパイラはあるコードが「未定義の動作」を起こしうる場合、"そのような未定義の動作は起こるはずがない"と解釈してコード最適化を行います。これは良い意味では、プログラマが間違ったコードを書くはずがないという前提に立って、最大限の最適化を行うと解釈できます。ただし、ひとたび間違った＝未定義の動作を含むコードに対しては、プログラマにとって予期しない結果をもたらすことがあります。

以下は、(日本語)関連記事へのリンクです:

  * [EXP30-C. 副作用が発生する式の評価順序に依存しない](https://www.jpcert.or.jp/sc-rules/c-exp30-c.html), [MSC15-C. 未定義の動作に依存しない](https://www.jpcert.or.jp/sc-rules/c-msc15-c.html)
  * [全てのCプログラマが未定義な振る舞いについて知っておくべきこと](http://blog-ja.intransient.info/2011/05/c-13.html)
  * [Old New Thing: 未定義動作はタイムトラベルを引き起こす（他にもいろいろあるけど、タイムトラベルが一番ぶっ飛んでる）](https://cpplover.blogspot.jp/2014/06/old-new-thing.html)


CC BY-SA 3.0
2017-10-30T05:53:15.813
39148
2017-10-30T06:08:45.990
2017-10-30T06:08:45.990
49
49
39144
answer


yohjpさんが説明されているようにあくまで未定義動作ですのでどのような結果でも受け入れるべきです。

その上で、手元のVisual Studio
2017では偶然ながら`1`が得られているので、あくまで一例として、この環境においてどのような解釈が行われているかについて説明します。

[code]

    int n = 1;
    n = --n / --n;
    // => 1
    
[/code]

ですが

[code]

    int n = 2;
    n = --n / --n;
    // => 実行時エラー
    
[/code]

[code]

    int n = 3;
    n = --n / --n;
    // => 1
    
[/code]

でしたので、`--n / --n`は

  1. 一つ目の`--n`により`n`をデクリメントする
  2. 二つ目の`--n`により`n`をもう１回デクリメントする
  3. 一つ目の`n`を読み込む
  4. 二つ目の`n`を読み込む（当然ながら3.と同じ値）
  5. `n / n`を実行する（当然ながら`1`）

という動作のようです。


CC BY-SA 3.0
2017-10-30T06:16:44.470
39150
2017-10-30T06:16:44.470
4236
39144
answer


そのサンプルはお世辞にも良いとは言えないですね。未定義動作ですから。

ここのストーリー的には２つのことが同時に挙げられているので余計にわからなくなってます。まずは１つ目

  * 部分式の評価順序

サンプル

[code]

    int f1(), f2(), f3();
    int x = f1() + f2() * f3();
    
[/code]

乗算と加算では乗算を先に行うわけですが、関数の呼び出し順序はそれとは無関係です。コンパイラは下記のどのコードを生成しても良いことになっています。

  * `f1()` → `f2()` → `f3()` の順に呼び、最後に乗算→加算する
  * `f2()` → `f3()` の順に呼びだし乗算して `f1()` を呼び加算する
  * `f3()` → `f2()` → `f1()` の順に呼び、最後に乗算→加算する
  * その他

関数に副作用があって、その発生順に意味があるときには注意が必要です。よく例に出されるのはマルチバイト文字の処理 `cp932` の全角を取り出したい場合に

[code]

    mbc = (getchar()<<8) | (getchar()); // 82 A0 の順に読み取って 82A0 にしたい
    
[/code]

としてしまうとバグってしまう場合があります。コンパイラは 右の `getchar()` を先に発行してもよいからです。そういう処理系では結果は
`0x82A0` でなくて `0xA082` となり文字化けします。評価順を確定させるには式を分けるのが一番簡単で

[code]

    mbc = getchar()<<8;
    mbc |= getchar();
    
[/code]

次に

  * 副作用の発生順序と位置

デクリメント演算子や代入演算子で変数の値が変化することを「副作用」 side-effect と言います。「式文」の主目的はこの副作用を得ることです。
[c](/questions/tagged/c "'c' のタグが付いた質問を表示") [c++](/questions/tagged/c%2b%2b
"'c++' のタグが付いた質問を表示") では、最適化の余地を最大にできるよう、この発生順序が決まっていません。

[code]

    int x=f1(), y=f2(), z;
    z=++x * y--;
    
[/code]

２行目の式には副作用が３つあります。

  * `x` の値を `+1` する
  * `y` の値を `-1` する
  * `z` の値を乗算の結果（ `+1` した後の `x` の値と、 `-1` する前の `y` の値の積）とする

この副作用が具体的にいつ発生するかは未規定 unspecified です。下記のどれも正しい動作です。ほとんどの場合にはどれであっても問題ありません。

  * `x` の値を `+1` して→乗算し→ `y` の値を `-1` し→乗算結果を `z` に代入する
  * `x+1` の値を求めて→乗算結果を `z` に代入し→ `y` の値を `-1` し→ `x` の値を更新する
  * `y` の現在値を一時保存し→ `y` の値を `-1` し→ `x` の値を `+1` し→ 乗算結果を `z` に代入する
  * その他

提示例題の場合も副作用が３つ [`n` を `-1` する] [`n` を `-1` する] [`n` に除算結果を代入する]
があり、どの順に発生させてもかまわないわけです。ですが、ここで言語仕様「副作用完了点と副作用完了点の間にて、１つのオブジェクトに複数の副作用が発生させるのは未定義動作」から、このプログラムはどんな結果になってもかまいません（例外発生して終了するのも、除算結果が
`42` になってもかまいません）

サンプルに出すにはよくないコードに当たってしまいましたね。ひとまずそのダメコードは忘れましょう。ダメなコードを書くと予想外な結果が出てしまうのが
[c](/questions/tagged/c "'c' のタグが付いた質問を表示") [c++](/questions/tagged/c%2b%2b
"'c++' のタグが付いた質問を表示") であると体験できたので良しとしておきます。


CC BY-SA 3.0
2017-10-30T06:24:57.787
39151
2017-10-30T09:45:00.127
2017-10-30T09:45:00.127
8589
8589
39144
answer