にゃははー

はへらー

演算子における引数の評価順と副作用

年も瀬だというのにTwitterで規格書たちが騒いでいたのは記憶に新しい(?)と思うけど、自分もC++11(とちょっとあと)までしか読んでなかったせいで、いろいろ時代の潮流に取り残されている感じがあったのでちゃんと自分で調べましたっていう話。

事の発端と反応はおおよそこちらにまとまっているので参照されたい。

paiza.hatenablog.com

現時点でC++17として正式に規格が発行されたわけじゃないので本文中ではC++1zとし、最終的にISとなった場合にさらに追加で変更されている可能性もありますので、注意してください。


さて、最初私がこの問題を見た時に思ったのは「C++14までならUBだけどC++1zだとどうだろうなぁ、おそらくUB?」というものでした。 というのもオペランドの評価順が決まったとか決まってないとかそういう話を聞いていたけど、同時に提案論文の全部が入ったわけでもないとも聞いていたので。 しかしそんなに気にすることでもない(既存の挙動とは異なるけどunspecified behaviourの一部がwell-definedになった程度だから困ることもない)だろうと思っていたので、読んでませんでした。

そのときはまぁそれで流してたんだけど、紆余曲折あって(他の話を眺めていたら結局同じようなところが問題になった)ちゃんと読まんとなぁと読んでみたらいろいろ分かったというまとめ*1

参照した文書はn4296(paizaが参照したもの)とn4618です。


さて、C++14までの関数呼び出しと演算子の評価順は一部を除いてunspecified behaviourであることは周知かと思います。 もうちょっと正確にn4296を参照して話すと、

  1. [over.match.oper](13.3.1.2 Operators in expressions) p.2

    Therefore, the operator notation is first transformed to the equivalent function-call notation as summarized in Table 10

    より、演算子は一回関数呼び出し形式に置き換えられる

  2. [expr.call](5.2.2 Function call) p.4

    When a function is called, each parameter (8.3.5) shall be initialized (8.5, 12.8, 12.1) with its corresponding argument. [Note: Such initializations are indeterminately sequenced with respect to each other (1.9) — end note]

    より、実引数の評価順は不定

という話です。 何事にも例外はあって、わかりやすい例だと

  • [expr.log.and](5.14 Logical AND operator) p.2 [expr.log.and](5.15 Logical OR operator) p.2

    The result is a bool. If the second expression is evaluated, every value computation and side effect associated with the first expression is sequenced before every value computation and side effect associated with the second expression.

  • [expr.cond](5.16 Conditional operator) p.1

    Every value computation and side effect associated with the first expression is sequenced before every value computation and side effect associated with the second or third expression.

  • [expr.comma](5.19 Comma operator) p.1

    Every value computation and side effect associated with the left expression is sequenced before every value computation and side effect associated with the right expression.

あたりかな。 ところどころに出てくるsequenced beforeというのがキモで、A is sequenced before Bと書かれていた場合は、Aは必ずBより前に完了するというもの。 何が前に起こるかは文脈に依存するけど、上の4例だとevery value computation and side effectって書かれているので、それらがもう一方のそれらよりも 前に完了するということ*2*3。 明示的に書いてない場合でもvalue computation and side effectと同等のことが言われていると考えて問題ないです。 逆に、value computationのみが書かれている場合もあるのでそっちは注意*4

これらはbuiltin operatorにのみ作用して、オーバーロードされたものに関しては[expr.call](5.2.2 Function call)が適用される。


さて話は変わってC++1z(n4618)となると、P0145R3の提案の一部が通って、しかしほとんど文言は変わって導入されている。 提案論文はちょっと強すぎたので。

じゃぁどういう形で入ったかを眺めてみると、[expr.call](5.2.2 Function call) p.5が新設されていて以下のように書かれている。

The postfix-expression is sequenced before each expression in the expression-list and any default argument. The initialization of a parameter, including every associated value computation and side effect, is indeterminately sequenced with respect to that of any other parameter. [ Note: All side effects of argument evaluations are sequenced before the function is entered (see 1.9). —end note ] [ Example:

void f() {
std::string s = "but I have heard it works even if you don’t believe in it";
s.replace(0, 4, "").replace(s.find("even"), 4, "only").replace(s.find(" don’t"), 6, "");
assert(s == "I have heard it works only if you believe in it"); // OK
}

—end example ] [ Note: If an operator function is invoked using operator notation, argument evaluation is sequenced as specified for the built-in operator; see 13.3.1.2. —end note ] [ Example:

struct S {
S(int);
};
int operator<<(S, int);
int i, j;
int x = S(i=1) << (i=2);
int y = operator<<(S(j=1), j=2);

After performing the initializations, the value of i is 2 (see 5.8), but it is unspecified whether the value of j is 1 or 2. —end example ]

つまり、演算子オーバーロードももともとの演算子の評価順規則が適用されますよってことです。 まぁオーバーロードしてもしなくても評価順は変わりませんよってことです。

最後の例でも書かれてる通り、関数呼び出し形式で明示的に書くとそれは演算子ではないので評価順は未規定ですよってなります。

で、勘のいい人だと「お、最後の例、シフト演算子sequenced beforeが定義されてるってことだよな」って思うと思います。 そしてそれは正解です。

という感じでそれぞれの演算子に追加されたものは追加されました。 変ないい方しましたが、つまり、C++1zでもunsequenced演算子はあるということです。

そしてそれがおおもとの話につながって、そもそも operator+がunsequencedのまま(評価順が規定されなかった) なのでインクリメント演算子value computation and side effectevaluation orderunspecified behaviourとなって、UBへと帰着するわけです。

めでたしめでたし。

*1:文章はまとまってないと思うけど内容はまとめたつもり

*2:そこら辺の実行順序関連に関しては[intro.execution](1.9 Program execution)にダヴァーっと書いてあるので頑張って読んでください

*3:もしくは2010-12-28 - Cry’s Diary。6年待ってるんですが続きが出ないので、続きが気になる皆さん、THE Axis of Evil (@Cryolite) | Twitterになんでもいいんで、とにかく「メモリモデルの続きをCry's Diaryに書け!」って感じでリプしまくってください。まじたのんます

*4:代入演算子とか