ページ 11

副作用、副作用完了点について

Posted: 2011年5月30日(月) 02:55
by sys
副作用と副作用完了点についての質問です。
(さきほど一度投稿したのですが、あまりにも間違いが多かったのでコメント頂く前に削除しました。)

まず、C++のJIS X3014:2003では、
  • 副作用完了点(sequence point)
    以前に行った評価に伴うすべての副作用が完了し、それ以後に行われる評価に伴うすべての副作用がまだ発生しない、実行列中の地点。副作用完了点には以下のようなものがある。
    ・完結式の直後
    ・関数の実引数の評価直後
    ・関数の返却値のコピー直後
    ・論理和演算子(||)または論理積演算子(&&)の左辺の評価直後
    ・コンマ演算子の左辺の評価直後
と定義されているみたいですが、C言語でも上記と同じと考えていいのでしょうか?

C++では、
  • JIS X3014:2003 1.9 プログラムの実行 16段落目
    完結式の評価完了時点は、それぞれ副作用完了点となる。
とはっきりと定義されているのを見つけたのですが、JIS X3010では式のどこが副作用完了点になるのかや、完結式の定義自体も見つけられませんでした。(関数呼出しの(),&&,||,?:,及びコンマ演算子に対しての規定を除いて)


CもC++の「完結式」と「完結式の評価完了時点は、それぞれ副作用完了点となる」が
それぞれ同じような意味で定義されていると仮定しての話ですが、具体例で考えてみました。
  • (1) i = i+1; ok
    (2) a = i; ok
    (3) i = ++i + 1;  未定義。i が副作用完了点(完結式の評価完了時点)の間で2度変更されているから
    (4) b = i + a[++i]; 未定義。副作用完了点(完結式の評価完了時点)の間で i が++iによる値の変更と、他の箇所で i を参照しているから(+演算子のオペランドの評価順序は未規定)


以下、特にわからない式

  • (5) a[i++] = i; 未定義。左辺値式では、格納される値を決定するため以外の目的(i++はオペランドに格納されている値を更新する副作用がある部分式)で i を読み取っているから
    (http://www.jpcert.or.jp/sc-rules/c-exp30-c.html より引用)

    (6) a[i++] = 5; 未定義。(5)から同じ理由で未定義となる?
    (7) b = i + a[i++]; 未定義。(5)から(4)とは違う理由で未定義となる?右辺値式で、格納される値を決定するため以外の目的(i++はオペランドに格納されている値を更新する副作用がある部分式)で i を読み取っているから、になる?

    (8) a[++i] = 5; ok? 左辺値式の++iは格納されている値を決定する目的で i を読み取っているから
    (9) b = a[++i]; ok? (8)と同じ理由から
    (10) a[++i] = i; 未定義? 副作用完了点(完結式の評価完了時点)の間で左辺値式で++iによる i の値の変更と、右辺値式で i を参照しているから(=演算子のオペランドの評価順序は未規定)


上記例については動作する動作しないではなく、規格に沿っているか、未定義とされているか
についての疑問です。
ご指摘、ご教示お願い致します。

Re: 副作用、副作用完了点について

Posted: 2011年5月30日(月) 04:36
by かずま
JIS X3010:2003 5.1.2.3 プログラムの実行

ボラタイルオブジェクトへのアクセス、オブジェクトの変更、ファイルの変更、又は
これらのいずれかの操作を行う関数の呼び出しは、すべて副作用(side effect) と呼
び、実行環境の状態に変化を生じる。式の評価は、副作用を引き起こしてもよい。副
作用完了点 (sequence point) と呼ばれる実行順序における特定の点において、それ
以前の評価に伴う副作用は、すべて完了していなければならず、それ以降の評価に伴
う副作用が既に発生していてはならない。(副作用完了点の要約を付属書C に示す。)

ISO/IEC 9899:1999 Annex C (informative)
Sequence points
The following are the sequence points described in 5.1.2.3:
— The call to a function, after the arguments have been evaluated (6.5.2.2).
— The end of the first operand of the following operators: logical AND && (6.5.13);
logical OR || (6.5.14); conditional ? (6.5.15); comma , (6.5.17).
— The end of a full declarator: declarators (6.7.5);
— The end of a full expression: an initializer (6.7.8); the expression in an expression
statement (6.8.3); the controlling expression of a selection statement (if or switch)
(6.8.4); the controlling expression of a while or do statement (6.8.5); each of the
expressions of a for statement (6.8.5.3); the expression in a return statement
(6.8.6.4).
— Immediately before a library function returns (7.1.4).
— After the actions associated with each formatted input/output function conversion
specifier (7.19.6, 7.24.2).
— Immediately before and immediately after each call to a comparison function, and
also between any call to a comparison function and any movement of the objects
passed as arguments to that call (7.20.5).


JIS X3010:2003 6.8 文およびブロック

完結式 (full expression) は、他の式の部分にも宣言子の部分にもなっていない式
とする。 初期化子、式文の中の式、選択文 (if 文又は switch文) の制御式、while
文又は do 文の制御式、for分の (勝利焼く可能な) 各式、及び return分の (省略可
能な) 式は完結式となる。完結式の終わりは、副作用完了点とする。


JIS X3010:2003 6.5 式

直前の副作用完了点から次の副作用完了点までの間に、式の評価によって一つのオブ
ジェクトに格納された値を変更する回数は、高々1回でなければならない。さらに、
変更前の値の読み取りは、格納される値を決定するためだけに行わなければならない。


(5) a[i++] = i; 未定義。
オブジェクト i は、後置++演算子によって、1回だけ変更されるが、その i の読み
取りは、後置++演算子以外に 代入演算子 = によっても行われるから。

(6) a[i++] = 5; ok。
オブジェクト i の読み取りは 1回、変更も 1回だから。

(7) b = i + a[i++]; 未定義。
オブジェクト i が後置++演算子によって変更されるが、その i の読み取りは、
取りは、後置++演算子以外に + 演算 によっても行われるから。

(8) a[++i] = 5; ok。
オブジェクト i の読み取りは 1回、変更も 1回だから。

(9) b = a[++i]; ok。
オブジェクト i の読み取りは 1回、変更も 1回だから。

(10) a[++i] = i; 未定義。
オブジェクト i は、前置++演算子によって、1回だけ変更されるが、その i の読み
取りは、前置++演算子以外に 代入演算子 = によっても行われるから。

Re: 副作用、副作用完了点について

Posted: 2011年5月30日(月) 08:17
by めるぽん
蛇足かもしれないですが、恐らく今年か来年あたりに策定される新しい C++ の仕様では、sequence point という用語は無くなり、代わりに sequenced-before という定義で、より厳格なモデルが規定されることになります。
http://d.hatena.ne.jp/Cryolite/20101228#p1

Re: 副作用、副作用完了点について

Posted: 2011年5月30日(月) 10:33
by sys
かずまさん宛
完結式は 6.8 で定義されていましたか。ありがとうございます。

再度の確認になりますが、、、
  • (1) a = ++i; ok オブジェクト i の読み取りは 1回、変更も 1回だから。
    (2) a = i++; ok オブジェクト i の読み取りは 1回、変更も 1回だから。

    (3) a = ++i+1; 未定義。 オブジェクト i が前置++演算子によって変更されるが、その i の読み取りは、前置++演算子以外に + 演算 によっても行われるから。
    (4) a = i+++1; 未定義。オブジェクト i が後置++演算子によって変更されるが、その i の読み取りは、後置++演算子以外に + 演算 によっても行われるから。

    (5) i = ++i; 未定義。オブジェクト i は前置++演算子によって、1回だけ変更されるが、その i の読み取りは、前置++演算子以外に代入演算子によっても行われるから。
    (6) i = i++; 未定義。オブジェクト i が後置++演算子によって、1回だけ変更されるが、その i の読み取りは、後置++演算子以外に代入演算子によっても行われるから。

    (7) a = ++++i; 未定義。オブジェクト i は前置++演算子によって、2回変更されるから

    (8) a = i++++; 未定義。オブジェクト i は後置++演算子によって、直前の副作用完了点から次の副作用完了までに2回変更されるから
[質問1]
a[i++] = i; の未定義の説明として、 
「左辺値式では、格納される値を決定するため以外の目的で i を読み取っているから」
ではなく、
「オブジェクト i は、後置++演算子によって、1回だけ変更されるが、その i の読み
取りは、後置++演算子以外に 代入演算子 = によっても行われるから。」
なので、
[ http://www.jpcert.or.jp/sc-rules/c-exp30-c.html ]のサイトの記載は間違っていると言える。

[質問2]
完結式の定義の
  • 「他の式の部分にも宣言子の部分にもなっていない式。」
を a[1] = i + 1; で説明すると、
a[1] ← 他の式(a[1] = i + 1)の部分
i + 1 ← 他の式(a[1] = i + 1)の部分
a[1] = i + 1 ← 完結式
という解釈で合っていますか?

[質問3]
  • 前置増分及び前置減分演算子 JIS X 6.5.2.4
    オペランドに格納されている値を更新する副作用は、
    直前の副作用完了点から次の副作用完了までの間に起こらなければならない。
後置++演算子の副作用は、次の副作用完了「まで」 なので完結式の終わり「まで」に起こる。
完結式の終わりは、副作用完了点と定義されているので
副作用完了点は、後置++演算子の副作用完了点 → 完結式の副作用完了点 という順番は定義されている。
「までの間」なので
a = ++i; と a = i++; の互いのaの結果の値が一緒でも規格に反してない、と言えますか?

[質問4]
前置++演算子は、部分式( a = ++i の右辺値式「++i」)の評価によってオペランドに格納されている値が更新されるので、副作用はないということですか?
(だから後置と違って定義(6.5.3.1)には副作用完了点について書かれてない?)

Re: 副作用、副作用完了点について

Posted: 2011年5月30日(月) 10:40
by sys
めるぽん さんが書きました:蛇足かもしれないですが、恐らく今年か来年あたりに策定される新しい C++ の仕様では、sequence point という用語は無くなり、代わりに sequenced-before という定義で、より厳格なモデルが規定されることになります。
http://d.hatena.ne.jp/Cryolite/20101228#p1
C++の定義は今回で初めて見ましたが、Cより複雑そうな印象を受けました。
参照サイト見ました。今後の参考にさせて頂きます。
ありがとうございます。

Re: 副作用、副作用完了点について

Posted: 2011年5月30日(月) 13:25
by かずま
sys さんが書きました: (3) a = ++i+1; 未定義。 オブジェクト i が前置++演算子によって変更されるが、その i の読み取りは、前置++演算子以外に + 演算 によっても行われるから。
2項+演算子のオペランドは、(++i) の結果と 1 であり、i の読み取りは行いません。
したがって、
(3) a = ++i+1; ok オブジェクト i の読み取りは 1回、変更も 1回だから。

(4) a = i++ +1; ok オブジェクト i の読み取りは 1回、変更も 1回だから。
sys さんが書きました: (5) i = ++i; 未定義。オブジェクト i は前置++演算子によって、1回だけ変更されるが、その i の読み取りは、前置++演算子以外に代入演算子によっても行われるから。
代入演算子の第1オペランドは i ですが、i の値の読み取りは行いません。
代入演算子の第2オペランドは (++I) の結果の値であって、i の値の読み取りは行いません。
代入演算子の第1オペランドは i なので、i の変更を行います。
i = ++i では、オブジェクト i の変更が、代入演算子によるものと、前置++演算子
によるものの 2回なので、変更する回数が高々 1回というのに反する。

したがって、
(5) i = ++i; 未定義。

(6) i = i++; 未定義。オブジェクト i が 2回変更されるから。
sys さんが書きました: (7) a = ++++i; 未定義。オブジェクト i は前置++演算子によって、2回変更されるから
未定義ではなく、エラー。
++++i は ++(++i) で、(++i) の結果が左辺値ではないので、最初の ++ はオペランドの制約に反する。

(8) a = i++++; も同様。
sys さんが書きました: [質問1]
a[i++] = i; の未定義の説明として、 
「左辺値式では、格納される値を決定するため以外の目的で i を読み取っているから」
間違い。
i++ で、i の読み取り 1回、変更 1回を行っているのに、代入演算子の第2オペラン
ドで i に格納される値を決定するため以外の目的で i を読み取っているから。

質問2 の解釈は合っています。
a[1]=i+1 という完結式(full expression) の部分式(sub expression)は、
a[1], i+1, a, 1, i, 1 です。
sys さんが書きました: 副作用完了点は、後置++演算子の副作用完了点 → 完結式の副作用完了点 という順番は定義されている。
a = i++ において i++ は完結式ではないので、「後置++演算子の副作用完了点」な
るものは存在しない。
sys さんが書きました: a = ++i; と a = i++; の互いのaの結果の値が一緒でも規格に反してない、と言えますか?
「a の結果の値が一緒でも」とはどういうこと?
i = 3; の場合、a = ++i の結果は 4、a = i++ の結果は 3。
sysX さんが書きました: [質問4]
前置++演算子は、部分式( a = ++i の右辺値式「++i」)の評価によってオペランドに格納されている値が更新されるので、副作用はないということですか?
a = ++1 という完結式において、++i という部分式には i の値を変更するという副作用があるから、完結式全体においても副作用はあります。

Re: 副作用、副作用完了点について

Posted: 2011年5月30日(月) 15:00
by かずま
かずま さんが書きました: (3) a = ++i+1; ok オブジェクト i の読み取りは 1回、変更も 1回だから。
ちょっと修正。
a = ++i + 1; ok。
オブジェクト i の変更のための読み取り 1回、変更 1回だから。

ついでに、
a = i + (i = 3); 未定義。
オブジェクト i の変更のためでない読み取り 1回、変更 1回だから。

Re: 副作用、副作用完了点について

Posted: 2011年5月30日(月) 23:49
by ISLe
なぜ未定義になるか、という点から考えてみてはどうでしょう?

未定義になるのは、
  • 代入演算子の各辺、二項演算子の各項は、評価される順序が決まっていない。
  • 前置(後置)++(--)演算子は、副作用完了点以前ではいつ値が変化するか決まっていない。
ということから、組み合わせがたくさんできてしまうのが原因です。

(追記)
後者は、例えばiが0のとき、++iを評価すると結果は1、だけど、iに格納されている値が1になるのは、いつか分かりません、ということです。
++iの結果を得るのが評価ですよ。

Re: 副作用、副作用完了点について

Posted: 2011年5月31日(火) 23:32
by sys
かずま さんが書きました: 代入演算子の第2オペランドは (++I) の結果の値であって、i の値の読み取りは行いません。

++++i は ++(++i) で、(++i) の結果が左辺値ではないので、最初の ++ はオペランドの制約に反する。
なるほど、よくわかりました。
これでだいたいの疑問が解消されました。

最初、a[i++] = i;の未定義の理由が
「左辺値式では、格納される値を決定するため以外の目的で i を読み取っているから」
と解説しているサイトがあり、右辺値式のiは関係ないように読み取れる気がしてしまい、
「変更前の値の読取りは、格納される値を決定するためだけに行われなければならない。」
と定義に書かれている「読取り」とはなんだ??と悩んでしまいました。
かずま さんが書きました:
sys さんが書きました: a = ++i; と a = i++; の互いのaの結果の値が一緒でも規格に反してない、と言えますか?
「a の結果の値が一緒でも」とはどういうこと?
i = 3; の場合、a = ++i の結果は 4、a = i++ の結果は 3。
これは、勘違いでした。
  • 「オペランドに格納されている値を更新する副作用は、直前の副作用完了点から次の副作用完了までの間に起こらなければならない。」
定義にこう書かれていたので、実際にiが1増えるというのが副作用完了までの間 = ただちに1増えてもいい?と勘違いして読んでいました。
ISLe さんが書きました: 後者は、例えばiが0のとき、++iを評価すると結果は1、だけど、iに格納されている値が1になるのは、いつか分かりません、ということです。
++iの結果を得るのが評価ですよ。
無駄に複雑に考えすぎていたのかもしれません。
b = i + a[++i];
iが評価される順番によってbの結果が変わっちゃうような式は未定義。
などともっとシンプルに考えれば解決しそうです。

なんとか理解出来たと思います。
かずまさん、ISLeさん、ありがとうございました。