c-sequence atcoder




なぜ、これらの構成要素は、事前増分および事後増分を使用して未定義の動作をしていますか? (10)

#include <stdio.h>

int main(void)
{
   int i = 0;
   i = i++ + ++i;
   printf("%d\n", i); // 3

   i = 1;
   i = (i++);
   printf("%d\n", i); // 2 Should be 1, no ?

   volatile int u = 0;
   u = u++ + ++u;
   printf("%d\n", u); // 1

   u = 1;
   u = (u++);
   printf("%d\n", u); // 2 Should also be one, no ?

   register int v = 0;
   v = v++ + ++v;
   printf("%d\n", v); // 3 (Should be the same as u ?)

   int w = 0;
   printf("%d %d %d\n", w++, ++w, w); // shouldn't this print 0 2 2

   int x[2] = { 5, 8 }, y = 0;
   x[y] = y ++;
   printf("%d %d\n", x[0], x[1]); // shouldn't this print 0 8? or 5 0?
}

Cは未定義の動作の概念を持っています。つまり、いくつかの言語構造は構文的には有効ですが、コード実行時の動作を予測することはできません。

私が知る限り、この標準では、未定義の動作の概念がなぜ存在するのかを明示的には言及していません。 私の考えでは、単純に言えば、言語設計者は、すべての実装がまったく同じ方法で整数オーバーフローを処理することを要求するのではなく、セマンティクスに余裕があることを望んでいたからです。 undefinedなので、整数のオーバーフローを引き起こすコードを書くと何かが起こる可能性があります。

それで、そのことを念頭に置いて、なぜこれらの「問題」がありますか? 言語によっては、特定のことが未定義の動作につながることが明らかになっています 。 問題はありません。「はず」はありません。 関与する変数の1つがvolatile宣言されているときに未定義の動作が変更された場合、それは何も証明も変更もしません。 それは未定義です。 あなたはその行動について推論することはできません。

あなたの最も興味深い例、

u = (u++);

は、定義されていない動作のテキストブックの例です( シーケンスポイントに関するWikipediaのエントリを参照)。


C標準では、変数は2つのシーケンスポイントの間に最大で1回だけ割り当てられるべきであると述べています。 たとえば、セミコロンはシーケンスポイントです。
したがって、フォームのすべてのステートメント:

i = i++;
i = i++ + ++i;

そのようなルールに違反している。 この標準では、振る舞いは未定義であり、未定義ではないとも言われています。 コンパイラの中にはこれらを検出して結果を出すものもありますが、これは標準ではありません。

ただし、2つの異なる変数を2つのシーケンスポイント間で増分することができます。

while(*src++ = *dst++);

上記は、文字列をコピー/解析する際の一般的なコーディング方法です。


a = a++a++ + a++ような式の構文は合法a++ + a++が、C標準のshallは従わないため、これらの構文の動作未定義です。 C99 6.5p2

  1. 前のシーケンスポイントと次のシーケンスポイントとの間で、オブジェクトは、表現の評価によって最大で1回修正された記憶値を持たなければならない。 さらに、前の値は、格納される値を決定するためにのみ読み出される[73]

脚注73では

  1. この段落は、

    i = ++i + 1;
    a[i++] = i;
    

    許可しながら

    i = i + 1;
    a[i] = i;
    

さまざまなシーケンスポイントがC11 (およびC99 )の附属書Cにリストされています。

  1. 以下は、5.1.2.3で説明したシーケンスポイントです。

    • ファンクションコールの実際の引数とファンクションデジグネータの評価の間。 (6.5.2.2)。
    • 次の演算子の第1オペランドと第2オペランドの評価の間:論理AND &&(6.5.13); 論理OR || (6.5.14); コンマ(6.5.17)。
    • 条件付きの最初のオペランドの評価の間? :第2オペランドと第3オペランドのどちらかが評価される(6.5.15)。
    • 完全な宣言者の終わり:宣言者(6.7.6);
    • 完全な式の評価と評価される次の完全な式の間。 以下は完全な式です:複合リテラル(6.7.9)の一部ではない初期化子。 式文(6.8.3)の式。 選択文の制御式(ifまたはswitch)(6.8.4); while文またはdo文の制御式(6.8.5)。 for文(6.8.5.3)の(オプションの)各式。 return文(6.8.6.4)の(オプションの)式。
    • ライブラリ関数が返る直前(7.1.4)。
    • 書式化された各入出力関数変換指定子(7.21.6,7.29.2)に関連付けられたアクションの後。
    • 比較関数への各呼び出しの直前および直後、および比較関数への呼び出しと、その呼び出しに引数として渡されたオブジェクトの移動(7.22.5)との間にも存在します。

C11の同じパラグラフの文言は:

  1. 同じスカラオブジェクトの異なる副作用または同じスカラーオブジェクトの値を使用する値計算のいずれかに対して、スカラオブジェクトの副作用が順序付けされていない場合、その動作は未定義です。 式の部分式の許容可能な順序が複数ある場合、その順序付けされていない副作用がいずれかの順序で発生すると、その動作は未定義です。

たとえば、最近のバージョンの-Wall-Werrorを使用して、プログラムでこのようなエラーを検出することができます-Werrorはプログラムをコンパイルすることを完全に拒否します。 以下はgcc(Ubuntu 6.2.0-5ubuntu12)6.2.0 20161005の出力です:

% gcc plusplus.c -Wall -Werror -pedantic
plusplus.c: In function ‘main’:
plusplus.c:6:6: error: operation on ‘i’ may be undefined [-Werror=sequence-point]
    i = i++ + ++i;
    ~~^~~~~~~~~~~
plusplus.c:6:6: error: operation on ‘i’ may be undefined [-Werror=sequence-point]
plusplus.c:10:6: error: operation on ‘i’ may be undefined [-Werror=sequence-point]
    i = (i++);
    ~~^~~~~~~
plusplus.c:14:6: error: operation on ‘u’ may be undefined [-Werror=sequence-point]
    u = u++ + ++u;
    ~~^~~~~~~~~~~
plusplus.c:14:6: error: operation on ‘u’ may be undefined [-Werror=sequence-point]
plusplus.c:18:6: error: operation on ‘u’ may be undefined [-Werror=sequence-point]
    u = (u++);
    ~~^~~~~~~
plusplus.c:22:6: error: operation on ‘v’ may be undefined [-Werror=sequence-point]
    v = v++ + ++v;
    ~~^~~~~~~~~~~
plusplus.c:22:6: error: operation on ‘v’ may be undefined [-Werror=sequence-point]
cc1: all warnings being treated as errors

重要な部分は、シーケンスポイントが何であるか、シーケンスポイントは何か、 そうでないもの何かを知ることです 。 たとえば、 コンマ演算子はシーケンスポイントなので、

j = (i ++, ++ i);

明確に定義されており、 iを1だけインクリメントして古い値を返し、その値を破棄します。 カンマ演算子で、副作用を解決します。 iを1だけインクリメントし、結果の値が式の値になります。つまり、これはj = (i += 2)を書く単なる考案された方法です。

i += 2;
j = i;

ただし、in関数の引数リストはカンマ演算子ではなく、別個の引数の評価の間にシーケンスポイントはありません。 代わりに、彼らの評価はお互いに関して順序付けされていません。 関数呼び出し

int i = 0;
printf("%d %d\n", i++, ++i, i);

関数の引数にi++++i評価の間にシーケンスポイントがないためi++の値は前と次のシーケンスポイントの間でi++++i両方で2回修正されるため、 未定義の動作をします。


ここでの答えのほとんどは、C標準から引用されており、これらの構文の振る舞いは未定義であることを強調しています。 これらの構文の動作が定義れていない理由を理解するには、まずC11標準に照らして以下の用語を理解してみましょう:

シーケンスされた: (5.1.2.3)

任意の2つの評価AB与えられた場合、 AB前にシーケンスされていれば、 Aの実行はBの実行の前に行われる。

順序付けられていない:

AB前後で配列決定されない場合、 AおよびBは配列決定されない。

評価は次の2つのうちの1つになります。

  • 値の計算は、式の結果を計算します。 そして
  • 副作用は 、オブジェクトの変更です。

シーケンスポイント:

AB評価の間のシーケンスポイントの存在は、 A関連するすべての値の計算および副作用がB関連するすべての値の計算および副作用の前に順序付けられることを意味する。

今質問にやってきます。

int i = 1;
i = i++;

標準によれば、

6.5式:

同じスカラオブジェクトの異なる副作用または同じスカラーオブジェクトの値を使用する値計算のいずれかに対して、スカラオブジェクトの副作用が順序付けされていない場合、 その動作は未定義です。 [...]

したがって、同じオブジェクトiに対する2つの副作用が相互に順序付けされていないため、上記の式はUBを呼び出します。 これは、 ++副作用の前または後に、 iへの代入による副作用が行われるかどうかが順序付けされていないことを意味し++
インクリメントの前後で割り当てが行われるかどうかによって、異なる結果が生成され、 未定義の動作の場合の1つになります

代入の左にあるi名前をil変更し、代入の右に(式i++irを代入すると、式は次のようになります

il = ir++     // Note that suffix l and r are used for the sake of clarity.
              // Both il and ir represents the same object.  

Postfix ++演算子に関する重要な点は次のとおりです。

変数の後に++が来るという理由だけで、インクリメントが遅くなるわけではありません 。 インクリメントは、コンパイラが元の値が使用されていることを保証している限り 、コンパイラが好むほど早くに発生する可能性があります

il = ir++は次のように評価することができます。

temp = ir;      // i = 1
ir = ir + 1;    // i = 2   side effect by ++ before assignment
il = temp;      // i = 1   result is 1  

または

temp = ir;      // i = 1
il = temp;      // i = 1   side effect by assignment before ++
ir = ir + 1;    // i = 2   result is 2  

2つの異なる結果1および2が得られ、これは割り当ておよび++による副作用の順序に依存し、したがってUBを呼び出す。


この種の計算で何が起こるかについての良い説明は、ISO W14サイトの文書n1188に記載されています。

私はそのアイディアを説明する。

この状況で適用される標準ISO 9899の主なルールは6.5p2です。

前のシーケンスポイントと次のシーケンスポイントとの間で、オブジェクトは、表現の評価によって最大で1回修正された記憶値を持たなければならない。 さらに、以前の値は、格納される値を決定するためにのみ読み出されるものとする。

i=i++ような式のシーケンスポイントは、 i=i++前とi++後にありi++

私が上で引用した論文では、小さなボックスによって形成されるプログラムを理解することができ、各ボックスに2つの連続したシーケンスポイントの間の命令が含まれていることが説明されています。 シーケンスポイントは標準の附属書Cで定義され、 i=i++場合、完全な表現を区切る2つのシーケンスポイントがある。 そのような表現は、文法のBackus-Naur形式のexpression-statement入力と構文的に同等です(文法は標準の付属書Aに示されています)。

したがって、ボックス内の命令の順序には明確な順序はありません。

i=i++

次のように解釈できます

tmp = i
i=i+1
i = tmp

または

tmp = i
i = tmp
i=i+1

コードi=i++を解釈するこれらのフォームは両方とも有効であり、両方とも異なる回答を生成するため、その動作は未定義です。

したがって、シーケンスポイントは、プログラムを構成する各ボックスの始めと終わりで見ることができます[ボックスはCの原子単位です]。ボックス内では、命令の順序はすべての場合に定義されていません。 その順序を変更すると結果が変わることがあります。

編集:

そのようなあいまいさを説明するための他の良い情報源は、 c-faqサイト( 本としても出版さています)のエントリーherehereherehereありhere


これに答えるもう一つの方法は、シーケンスポイントと未定義の行動の秘密の細部にうんざりしているのではなく、単に何を意味するのかという質問だけです。 プログラマは何をしようとしていましたか?

i = i++ + ++iについての最初の断片は、私の本ではかなり狂っています。 誰もが実際のプログラムにそれを書いていることはありません。それが何をするのかは明らかではありません。誰かがこのような意図的な操作シーケンスにつながったと考えている可能性のあるアルゴリズムはありません。 コンパイラが何をするべきかを理解できなければ、私の本ではそれはあなたと私には分かりません。

2番目の断片、 i = i++は理解しやすくなります。 誰かが明らかにiをインクリメントして、結果をiに戻そうとしています。 しかし、Cでこれを行うにはいくつかの方法があります。1をiに追加し、結果をiに割り当てる最も基本的な方法は、ほぼすべてのプログラミング言語で同じです。

i = i + 1

もちろん、Cには便利なショートカットがあります:

i++

これは、「1をiに加算し、その結果をiに戻す」という意味です。 だから、私たちが2人の人をぶつけ合うと

i = i++

私たちが本当に言っているのは、1をiに加え、結果をiに割り当て、結果をiに戻すことです。 私たちは混乱しているので、コンパイラが混乱してもあまり気にしません。

現実的に、これらの狂った表現が書かれる唯一の時間は、人々が++をどのように動作させるかの人工的な例としてそれらを使用しているときです。 もちろん、++の仕組みを理解することが重要です。 しかし、++を使用するための1つの実際的なルールは、「++を使用する式が何を意味するのかが明白でない場合は、書き込まないでください。

私たちはcomp.lang.cで数え切れないほどの時間を費やしていました。 理由を説明しようとする私の長い答えのうちの2つは、Web上にアーカイブされています。


コードの行をコンパイルして逆アセンブルするだけで、もしあなたが得ているものがどれほど正確であるか知りたいのであれば、

これは私のマシン上で、私は何が起こっていると思いますか?

$ cat evil.c
void evil(){
  int i = 0;
  i+= i++ + ++i;
}
$ gcc evil.c -c -o evil.bin
$ gdb evil.bin
(gdb) disassemble evil
Dump of assembler code for function evil:
   0x00000000 <+0>:   push   %ebp
   0x00000001 <+1>:   mov    %esp,%ebp
   0x00000003 <+3>:   sub    $0x10,%esp
   0x00000006 <+6>:   movl   $0x0,-0x4(%ebp)  // i = 0   i = 0
   0x0000000d <+13>:  addl   $0x1,-0x4(%ebp)  // i++     i = 1
   0x00000011 <+17>:  mov    -0x4(%ebp),%eax  // j = i   i = 1  j = 1
   0x00000014 <+20>:  add    %eax,%eax        // j += j  i = 1  j = 2
   0x00000016 <+22>:  add    %eax,-0x4(%ebp)  // i += j  i = 3
   0x00000019 <+25>:  addl   $0x1,-0x4(%ebp)  // i++     i = 4
   0x0000001d <+29>:  leave  
   0x0000001e <+30>:  ret
End of assembler dump.

(私は... 0x00000014命令がある種のコンパイラ最適化であったとします)


多くの場合、この質問は、次のようなコードに関連する質問の重複としてリンクされています。

printf("%d %d\n", i, i++);

または

printf("%d %d\n", ++i, i++);

または類似の変異体である。

これは既に述べたように未定義の振る舞いですが、 printf()が次のような文と比較したときに微妙な違いがあります。

   x = i++ + i++;

次の文では、

printf("%d %d\n", ++i, i++);

printf()における引数の評価順序unspecified 。 つまり、式i++++iは、どのような順序で評価されてもかまいません。 C11標準にはこれに関するいくつかの関連する説明があります:

附属書J、不特定の行動

引数内の関数指定子、引数、およびサブ式が関数呼び出し(6.5.2.2)で評価される順序。

3.4.4、不特定の振る舞い

不特定の値の使用、またはこの国際標準が2つ以上の可能性を提供し、いかなる場合においてもそれが選択されるさらなる要件を課さない場合のその他の行動。

例指定されていない振る舞いの例は、関数への引数が評価される順序です。

不特定の動作自体は問題ではありません。 この例を考えてみましょう。

printf("%d %d\n", ++x, y++);

++xy++の評価順序が不定であるため、これも不特定の動作をします。 しかし、それは完全に合法で有効な声明です。 このステートメントに未定義の動作はありません 。 変更( ++xy++ )は別個のオブジェクトに対して行われるためです。

次のステートメントをレンダリングするもの

printf("%d %d\n", ++i, i++);

定義されていない振る舞いは、これらの2つの式が介在するシーケンスポイントなしで同じオブジェクトiを変更するという事実です。

もう1つの詳細は、printf()呼び出しに含まれるコンマ区切りであり、 コンマ演算子ではないことです。

カンマ演算子では、オペランドの評価の間にシーケンスポイントが導入されるため、これは重要な違いです。

int i = 5;
int j;

j = (++i, i++);  // No undefined behaviour here because the comma operator 
                 // introduces a sequence point between '++i' and 'i++'

printf("i=%d j=%d\n",i, j); // prints: i=7 j=6

カンマ演算子は、オペランドを左から右に評価し、最後のオペランドの値のみを返します。 したがって、 j = (++i, i++);++i i6増やし、 i++j割り当てられたi6 )の古い値を生成する。 それからiはポストインクリメントのために7なります。

したがって、関数呼び出しのコンマがカンマ演算子である場合

printf("%d %d\n", ++i, i++);

問題になることはありません。 しかし、 カンマセパレータであるため、 未定義の動作が発生します

未定義のビヘイビアを初めて知った人にとっては、 未定義のビヘイビア について知っておくべきCプログラマは、Cの未定義ビヘイビアのコンセプトや他の多くのバリエーションを理解することで利益を得るでしょう。

この投稿: 未定義、不特定、実装定義の振る舞いも関連しています。


私は、C99規格の関連部分は6.5式、§2

前のシーケンスポイントと次のシーケンスポイントとの間で、オブジェクトは、表現の評価によって最大で1回修正された記憶値を持たなければならない。 さらに、以前の値は、格納される値を決定するためにのみ読み出されるものとする。

および6.5.16代入演算子、§4:

オペランドの評価の順序は不特定である。 代入演算子の結果を変更しようとした場合、または次のシーケンスポイントの後にアクセスしようとすると、その動作は未定義です。


あなたの質問は、おそらく "なぜこれらの構文はCで未定義の動作ですか?"ではありませんでした。あなたの質問は、おそらく "なぜこのコード(++私)が私に期待した価値を与えてくれなかったのですか?"と誰かがあなたの質問に重複してマークし、あなたをここに送りました。

この答えはその質問に答えようとしています。なぜあなたのコードはあなたに期待した答えを与えてくれなかったのですか?また、期待どおりに動作しない式を認識(そして回避)する方法を学ぶことができます。

C ++--演算子の基本的な定義を今までに聞いたことがあり、接頭辞の形式++xと後置の形式がどのように違うのか聞いたことがあると思いますx++。しかし、これらの演算子は考えるのが難しいので、あなたが理解していることを確認するために、おそらくあなたは小さなテストプログラムを書きました。

int x = 5;
printf("%d %d %d\n", x, ++x, x++);

しかし、あなたの驚いたことに、このプログラムはあなたが理解するのを助けませんでした - それ++は、あなたが思ったこととは全く異なる何かをしているかもしれないことを示唆して、

あるいは、おそらくあなたは理解しにくい表現を見ているでしょう。

int x = 5;
x = x++ + ++x;
printf("%d\n", x);

おそらく誰かがあなたにそのコードをパズルとして与えたでしょう。このコードは、特にあなたがそれを実行する場合には意味がありません。また、2つの異なるコンパイラでコンパイルして実行すると、2つの異なる答えが得られる可能性があります。どうしたの?正しい答えはどれですか?(そして答えは、両方があるか、どちらもないということです)。

これまでに聞いたように、これらの式はすべて定義されていません。つまり、C言語は何をするのかについて保証しません。これは奇妙で驚くべき結果です。なぜなら、コンパイルして実行している限り、書くことができるプログラムは、一意かつ明確な出力を生成すると考えていたからでしょう。しかし、未定義の振る舞いの場合、そうではありません。

何が式を定義しないのですか?表現は関係して++おり、--常に未定義ですか?もちろんそうではありません。これらは便利な演算子であり、正しく使用すると完全に定義されています。

私たちがそれらを未定義にする表現については、何が起こるのかがわからないときに、あまりにも多くのことが起こっているときです。

私はこの答えで使った2つの例に戻りましょう。私が書いたとき

printf("%d %d %d\n", x, ++x, x++);

問題は、呼び出しの前にprintf、コンパイラがx最初の値を計算するかx++、または、おそらくそれを計算します++xか?しかし、われわれは分かりません。Cには、関数の引数が左から右、右から左、または他の順序で評価されるというルールはありません。だから我々は、コンパイラがどうなるかを言うことができないx最初の、そして++x、その後x++、またはx++その後、++xその後x、または他のいくつかのため。しかし、順序は明らかに重要です。なぜなら、コンパイラが使用する順序に応じて、別の結果を明確に表示するからprintfです。

この狂った表現はどうですか?

x = x++ + ++x;

この式の問題は、xの値を変更する3つの異なる試みが含まれていることです。(1)x++xに1を追加し、新しい値を格納しx、古い値を返しxます。 (2)++xパーツはxに1を加えて新しい値を格納し、新しい値xを返しxます。 (3)x =他の2つの合計をxに戻そうとする。 3つの試みられた課題のどれが「勝つ」か。 3つの値のどちらに実際に割り当てられxますか?繰り返しになりますが、おそらく驚くべきことに、Cには私たちに言いたいことはありません。

優先順位や連合性、左から右への評価が、どのような順序で起こるかを示しているが、そうではないことを想像するかもしれません。あなたは私のことを信じていないかもしれませんが、私の言葉をもう一度言います。先行性と連想性は、Cの式の評価順序のあらゆる側面を決定するわけではありません。特に、私たちがx優先順位や連想性などに新しい価値を割り当てようとしている別の場所は、それらの試みのどれが最初に起こるか、最後に起こるか、何かを教えてくれませ

だから、すべてのあなたのプログラムが明確に定義されていること、どの表現を書くことができるのか、書くことができないことを確認したいのですか?

これらの表現はすべて問題ありません。

y = x++;
z = x++ + y++;
x = x + 1;
x = a[i++];
x = a[i++] + b[j++];
x[i++] = a[j++] + b[k++];
x = *p++;
x = *p++ + *q++;

これらの式はすべて未定義です。

x = x++;
x = x++ + ++x;
y = x + x++;
a[i] = i++;
a[i++] = i;
printf("%d %d %d\n", x, ++x, x++);

最後の質問は、どの式が明確に定義されているか、どの式が定義されていないのか、

前に述べたように、定義されていない表現は、あまりにも多くのことが起こっていて、何が起こったのか、そしてその順序が重要であるかどうかはわかりません。

  1. 2つ以上の異なる場所で変更されている(割り当てられている)変数が1つある場合は、どの変更が最初に起こるかをどのように知っていますか?
  2. 変数がある場所で変更され、その値が別の場所で使用されている場合、古い値または新しい値が使用されているかどうかをどのように知っていますか?

#1の例として、式

x = x++ + ++x;

`xを修正しようとする試みが3回あります。

#2の例として、式

y = x + x++;

我々は両方の値を使用し、xそれを変更する。

これは答えです:あなたが書く式では、各変数は多くても一度しか変更されません。また、変数が変更された場合は、その変数の値を他の場所で使用しようともしません。





sequence-points