これらのコンストラクトが事前および事後のインクリメントの未定義の動作を使用するのはなぜですか?


815
#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\n", ++w, w); // shouldn't this print 1 1

   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?
}

12
@Jarett、いや、「シーケンスポイント」へのいくつかのポインタが必要でした。作業中にi = i ++のコードを見つけましたが、「これはiの値を変更するものではありません」と思いました。私はテストし、なぜだろうと思いました。それ以来、この文を削除してi ++に置き換えました。
PiX 2009年

198
質問者が問題の構成を使用したいので、誰もが常にこのような質問が行われると想定していることは興味深いと思います。私の最初の仮定は、PiXがこれらが悪いことを知っているということでした。 .. JCF(ジャンプアンドキャッチファイア)を含む
ブライアンポストウ

32
興味津々:コンパイラーが "u = u ++ + ++ u;"のような構成要素について警告しないのはなぜですか?結果が未定義の場合?
OpenGL ESを学ぶ

5
(i++)括弧に関係なく、依然として1と評価されます
Drew McGowen 2013

2
i = (i++);をするつもりでも、それを書くための明確な方法は確かにあります。それが明確に定義されていても、それは真実です。の動作を定義するJavaでさえ、それはi = (i++);まだ悪いコードです。ただ書くi++;
キース・トンプソン

回答:


566

Cには未定義の動作の概念があります。つまり、一部の言語構成は構文的には有効ですが、コードが実行されたときの動作を予測することはできません。

私の知る限り、この規格では、未定義の動作の概念が存在する理由を明確に述べていません。私の考えでは、言語の設計者がセマンティクスに余裕を持たせたいと思っているだけです。つまり、すべての実装で整数オーバーフローをまったく同じ方法で処理する必要があり、深刻なパフォーマンスコストを課す可能性が高いため、動作をそのままにしました。定義されていないため、整数オーバーフローを引き起こすコードを記述した場合、何かが発生する可能性があります。

では、それを念頭に置いて、なぜこれらの「問題」なのでしょうか。言語は、特定のものが未定義の動作につながることを明確に述べています。問題はなく、「すべき」もありません。関係する変数の1つが宣言されたときに未定義の動作がvolatile変化しても、何も証明または変更されません。それは未定義。あなたはその行動について推論することはできません。

あなたの最も興味深く見える例、

u = (u++);

未定義の動作の教科書の例です(シーケンスポイントに関するWikipediaのエントリを参照)。


8
@PiX:考えられる理由がいくつかあるため、物事は未定義です。これらには、明確な「正しい結果」がない、異なるマシンアーキテクチャが異なる結果を強く支持する、既存の慣行が一貫していない、または標準の範囲を超えている(どのファイル名が有効かなど)。
Richard

ただみんなを混乱させるために、そのようないくつかの例は現在C11で明確に定義されていi = ++i + 1;ます。
2014

2
標準と公開された根拠を読んで、UBの概念が存在する理由は明らかです。標準は、C実装が特定の目的(「1つのプログラム」規則の説明を参照)に適するために実行する必要があるすべてを完全に説明することを意図したものではなく、実装者の判断と有用な品質の実装を作成したいという願望に依存しています。低レベルのシステムプログラミングに適した高品質の実装では、ハイエンドの数値のcrunching.applicationsでは必要とされないアクションの動作を定義する必要があります。標準を複雑にしようとするのではなく...
スーパーキャット2017

3
...どのコーナーケースが定義されているか定義されていないかについて極端に詳細に説明することにより、標準の作成者は、実装者は、サポートすることが期待されるプログラムの種類によって必要とされる動作の種類を判断するために、より適切なペースをとるべきであると認識しました。ハイパーモダニストコンパイラは、特定のアクションを実行することは、質の高いプログラムがそれらを必要としないことを暗示することを意図したものであると偽っていますが、標準と根拠は、そのような想定された意図と矛盾しています。
スーパーキャット2017

1
@jrh:ハイパーモダニストの哲学がいかに手に負えなくなったかに気付く前に、私はその答えを書きました。私をいらいらさせているのは、「必要なプラットフォームはとにかくそれをサポートできるので、この動作を正式に認識する必要がない」から「認識されなかったために使用可能な置き換えを提供せずにこの動作を削除できるため、コードはすべて必要だった」多くの振る舞いはずっと良い方法で置き換えを支持してずっと前廃止されるべきでしたが、それは彼らの正当性を認める必要があったでしょう。
スーパーキャット2018年

78

コード行をコンパイルして逆アセンブルするだけです。正確にそれを知る傾向がある場合は、取得したものを取得できます。

これは私が私のマシンで得ているものであり、私が起こっていると思うものと一緒です:

$ 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命令が何らかのコンパイラの最適化だったとしたら?)


どうすればマシンコードを取得できますか?私はDev C ++を使用していますが、コンパイラー設定で「コード生成」オプションを
いじってみまし

5
@ronnieaka gcc evil.c -c -o evil.bingdb evil.bindisassemble evil、またはWindowsで同等のものは
どれでも

21
この回答は、実際にはの問題には対応していませんWhy are these constructs undefined behavior?
Shafik Yaghmour 2014

9
余談ですが、gcc -S evil.cここで必要なのは、(を使用して)アセンブリにコンパイルする方が簡単です。それを組み立ててから分解することは、それを行うための単なる迂回的な方法です。
2015

50
記録のために、何らかの理由で特定の構成体が何をするのか疑問に思っている場合、特にそれが未定義の動作である可能性があるという疑いがある場合、「コンパイラで試してみてください」という古くからのアドバイスは潜在的にかなり危険です。せいぜい、今日のこれらの状況下で、このバージョンのコンパイラーでそれが何をするかを学びます。あなたがそれが何をすることが保証されているかについて何かを学ぶなら、あなたは多くを学ぶことはないでしょう。一般に、「コンパイラーで試してみる」だけでは、コンパイラーでのみ機能する移植性のないプログラムになります。
スティーブサミット

64

C99標準の関連部分は6.5式、§2だと思います

前のシーケンスポイントと次のシーケンスポイントの間で、オブジェクトは、式の評価によって、格納された値を最大で1回変更する必要があります。さらに、以前の値は、格納される値を決定するためにのみ読み取られるものとします。

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

オペランドの評価順序は指定されていません。代入演算子の結果を変更しようとしたり、次のシーケンスポイントの後にアクセスしたりした場合の動作は未定義です。


2
上記は「i = i = 5;」が未定義の動作になることを意味しますか?
supercat

1
@supercatは、私の知る限り、i=i=5未定義の動作です
dhein

2
@Zaibis:ほとんどの場所のルールで使用したい根拠は、理論的にはマルチプロセッサプラットフォームがA=B=5;「書き込みロックA;書き込みロックB;ストア5からA;ストア5からB;ロック解除B」のようなものを実装できるということです。 ; Unock A; "、およびC=A+B;" Read-lock A; Read-lock B; Compute A + B; Unlock A and B; Unlock A; Store result; Unlock C; "のようなステートメント。これにより、あるスレッドがA=B=5;別のスレッドを実行した場合C=A+B;、後者のスレッドは両方の書き込みが行われたと見なすか、どちらも行わなかったと見なします。潜在的に有用な保証。I=I=5;ただし、1つのスレッドが実行した場合、...
supercat

1
...コンパイラは、両方の書き込みが同じ場所への書き込みであることを認識しませんでした(一方または両方の左辺値にポインタが含まれている場合、判別が難しい場合があります)、生成されたコードはデッドロックする可能性があります。実際の実装では通常の動作の一部としてこのようなロックを実装しているとは思いませんが、標準ではそれは許容されます。ハードウェアがそのような動作を安価に実装できれば、それは役立つでしょう。今日のハードウェアでは、そのような動作はデフォルトとして実装するにはコストがかかりすぎますが、常にそうであるとは限りません。
スーパーキャット2013

1
@supercatですが、c99のシーケンスポイントアクセスルールだけでは、未定義の動作として宣言できませんか?それで、ハードウェアが技術的に何を実装できるかは問題ではありませんか?
dhein 2013

55

ここでの回答のほとんどは、C標準から引用されており、これらの構成体の動作は未定義であることを強調しています。これらの構造の動作が未定義である理由を理解するために、 C11標準に照らして最初にこれらの用語を理解しましょう。

シーケンス:(5.1.2.3)

任意の二つの評価を与えられたABすれば、A前に配列決定されB、その後の実行はAの実行に先行するものB

シーケンスなし:

がのA前または後Bにシーケンスされていない場合、AおよびBはシーケンスされません。

評価は次の2つのいずれかになります。

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

シーケンスポイント:

式の評価の間の配列点の存在AとはB意味し、そのすべての値の計算副作用に関連付けられているAすべての前に配列決定された値の計算副作用と関連しますB

次のような表現について質問します

int i = 1;
i = i++;

標準はそれを言う:

6.5式:

スカラーオブジェクトの副作用が同じスカラーオブジェクトの異なる副作用または同じスカラーオブジェクトの値を使用した値の計算に関連してシーケンスされていない場合、動作はundefinedです。[...]

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

i代入の左側がbe ilで、代入の右側が(式i++)でbeの名前を変更すると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が呼び出されます。


52

未指定の動作未定義の動作の両方を呼び出すため、動作を実際に説明することはできません。したがって、このコードに関する一般的な予測を行うことはできませんが、Deep CUnspecifiedやUndefinedなどのOlve Maudalの作品を読む、うまくいくことがあります特定のコンパイラと環境での非常に特殊なケースで推測しますが、本番環境の近くでは行わないでください。

上を移動するように、不特定の行動には、標準C99ドラフトセクション6.5のパラグラフ3は言う(強調鉱山):

演算子とオペランドのグループ化は構文で示されます。74)後で指定される場合を除いて(関数呼び出し()、&&、||、?:、およびコンマ演算子の場合)、部分式の評価の順序とどの副作用が発生するかはどちらも指定されていません。

したがって、次のような行がある場合:

i = i++ + ++i;

私たちは、かどうかわからないi++か、++i最初に評価されます。これは主にコンパイラーに最適化のためのより良いオプションを与えるためです。

我々はまた、持っている未定義の動作をプログラム変数を変更する(しているので、ここでも同様にiu間に複数回、等。)シーケンスポイント。標準草案の6.52項から(強調鉱山)から:

前のシーケンスポイントと次のシーケンスポイントの間で、オブジェクトは、式の評価によって、格納された値を最大で1回変更する必要があります。さらに、以前の値は、格納される値を決定するためにのみ読み取られるものとします

次のコード例は未定義として引用されています。

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

これらのすべての例で、コードは同じシーケンスポイントでオブジェクトを複数回変更しようとしています。これ;により、これらの各ケースで終了します。

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

i = (i++);
^    ^

u = u++ + ++u;
^   ^       ^

u = (u++);
^    ^

v = v++ + ++v;
^   ^       ^

未指定の動作は、セクションのc99標準案で次のように定義されて3.4.4います。

未指定の値の使用、またはこの国際標準が2つ以上の可能性を提供し、どのインスタンスでも選択されるものにそれ以上の要件を課さないその他の動作

そして、未定義の動作は、セクションで定義されています3.4.3ように:

この国際標準が要件を課さない、移植不能またはエラーのあるプログラム構造またはエラーのあるデータの使用時の動作

そしてそれに注意してください:

考えられる未定義の動作は、予測できない結果で完全に状況を無視することから、環境に特徴的な文書化された方法(診断メッセージの発行の有無にかかわらず)での変換またはプログラムの実行中の動作、変換または実行の終了(発行あり)までさまざまです。診断メッセージの)。


33

これに答える別の方法は、シーケンスポイントと未定義の動作の難解な詳細に行き詰まるのではなく、単に尋ねることです。それら何を意味しているのでしょうか? プログラマは何をしようとしていましたか?

について尋ねられた最初のフラグメントはi = i++ + ++i、私の本ではかなり明らかに正気ではありません。誰もそれを実際のプログラムで書くことは決してありません、それが何をするかは明らかではありません。誰かがこの特定の人為的な操作のシーケンスをもたらすコード化を試みた可能性のあるアルゴリズムはありません。そして、あなたと私にはそれが何をするべきか明白ではないので、コンパイラーが何をすべきかを理解できなくても、私の本では問題ありません。

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

i = i + 1

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

i++

これは、「iに1を追加し、結果をiに割り当てる」ことを意味します。したがって、2つのホッジポッジを作成する場合、

i = i++

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

現実的には、これらのクレイジーな式が書かれるのは、++がどのように機能するかの人工的な例として人々がそれらを使用しているときだけです。そしてもちろん、++がどのように機能するかを理解することは重要です。しかし、++を使用するための1つの実際的なルールは、「++を使用する式が何を意味するのかが明らかでない場合は、記述しないでください」です。

私たちはcomp.lang.cに数え切れないほどの時間を費やして、このような式とそれらが未定義である理由を議論していました。理由を本当に説明しようとする私の長い回答のうちの2つは、Webにアーカイブされています。

質問3.8と、C FAQリストのセクション3にある残りの質問も参照してください。


1
未定義の動作に関するやや厄介な問題は、コンパイラの99.9%では以前は安全でしたが、それがもはや当てはまらないこと*p=(*q)++;を意味するというif (p!=q) *p=(*q)++; else *p= __ARBITRARY_VALUE;ことです。ハイパーモダンCは*p、前者で提供するために使用されるコンパイラーの効率レベルを達成するために、後者の定式化のようなものを書く必要があります(ただし、コードが何を気にしないことを示す標準的な方法はありません)(このelse句は、ifコンパイラーは、一部の新しいコンパイラーが必要とするものを最適化します。
スーパーキャット2015年

@supercat私は今、その種の最適化を実行するのに十分な「スマート」なコンパイラーは、assertステートメントを覗くのにも十分スマートでなければならないので、プログラマーは問題の行の前に単純なを付けることができassert(p != q)ます。(もちろん、そのコースを受講するに<assert.h>は、デバッグ以外のバージョンでアサーションを完全に削除せずに__builtin_assert_disabled()、コンパイラーが認識できるようなものに変えて、コードを発行しないように書き換える必要もあります。)
Steve Summit

25

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

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

または

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

または同様の亜種。

これもすでに述べたように未定義の動作ですが、次のようなprintf()ステートメントと比較すると、微妙な違いがあります。

x = i++ + i++;

次のステートメントでは:

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

の引数の評価順序printf()不定です。つまり、式i++++iは任意の順序で評価できます。C11標準には、これに関するいくつかの関連説明があります。

附属書J、不特定の行動

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

3.4.4、不特定の動作

未指定の値の使用、またはこの国際標準が2つ以上の可能性を提供し、どのインスタンスでどれを選択するかについてさらなる要件を課さないその他の動作。

例不特定の動作の例は、関数への引数が評価される順序です。

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

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

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

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

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

未定義の動作これらの2つの式は修正するという事実である同じオブジェクトをi介在せずにシーケンスポイント


別の詳細は、ということであるコンマの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インクリメントi6i++生成します。その後、ポストインクリメントのためになります。i6ji7

その場合はカンマ関数呼び出しでは、カンマ演算子であることをしました

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

問題にはなりません。ただし、ここではコンマ区切り文字であるため、未定義の動作を呼び出します。


未定義の動作不慣れな人にとっては、Cの未定義の動作 の概念やその他の多くの変形を理解するために、すべてのCプログラマが未定義の動作について知っておくべきことを読むとよいでしょう。

この投稿:未定義、未指定、実装定義の動作も関連しています。


このシーケンスint a = 10, b = 20, c = 30; printf("a=%d b=%d c=%d\n", (a = a + b + c), (b = b + b), (c = c + c));は安定した動作をするように見えます(gcc v7.3.0での右から左への引数評価;結果 "a = 110 b = 40 c = 60")。それは、割り当てが「フルステートメント」と見なされ、シーケンスポイントが導入されるためですか?それは左から右への引数/ステートメントの評価をもたらすべきではありませんか?または、それは未定義の動作の単なる現れですか?
カバディアス

@kavadiasそのprintfステートメントには、上記と同じ理由で、未定義の動作が含まれます。あなたはそれぞれ3番目と4番目の引数を書いbていcて、2番目の引数を読んでいます。ただし、これらの式の間にシーケンスはありません(2番目、3番目、4番目の引数)。gcc / clangには、-Wsequence-pointこれらの検索にも役立つオプションがあります。
PPの

23

コンパイラやプロセッサが実際にそうすることはまずありませんが、C標準では、コンパイラが次のシーケンスで「i ++」を実装することは合法です。

In a single operation, read `i` and lock it to prevent access until further notice
Compute (1+read_value)
In a single operation, unlock `i` and store the computed value

そのようなことを効率的に行うためのハードウェアをサポートしているプロセッサはないと思いますが、そのような動作によってマルチスレッドコードが容易になる状況は容易に想像できます(たとえば、2つのスレッドが上記を実行しようとすると、シーケンスは同時に、i2ずつ増加します)、将来のプロセッサがそのような機能を提供する可能性があることはまったく考えられません。

コンパイラーがi++上記のように記述し(標準では合法)、式全体の評価(上記も合法)の評価全体にわたって上記の命令を散在させ、他の命令の1つが発生したことに気付かなかった場合にアクセスするiために、コンパイラがデッドロックする一連の命令を生成することが可能(かつ合法)になります。確かに、コンパイラーはほぼ確実iに両方の場所で同じ変数が使用されている場合に問題を検出しますが、ルーチンが2つのポインターpand への参照を受け入れ、上記の式で(qおよび使用するの(*p)(*q)はなく)i2回)同じオブジェクトのアドレスが両方に渡された場合に発生するデッドロックを認識または回避するためにコンパイラは必要ありません。pとしますq


16

またはのような式の構文は有効ですが、C標準のshallには準拠していないため、これらの構文動作定義されていません。C99 6.5p2a = a++a++ + a++

  1. 前のシーケンスポイントと次のシーケンスポイントの間で、オブジェクトは、式の評価によって、格納された値を最大で1回変更する必要があります。[72]さらに、以前の値は、格納される値を決定するためにのみ読み取られるものとします[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)。
    • 条件付き?の第1オペランドの評価の間 :演算子と、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. 同じスカラーオブジェクトの異なる副作用または同じスカラーオブジェクトの値を使用した値の計算のいずれかに対して、スカラーオブジェクトの副作用がシーケンスされていない場合、動作は未定義です。式の部分式の許容可能な順序付けが複数ある場合、そのような順序付けられていない副作用が順序付けのいずれかで発生しても、動作は未定義です。84)

あなたは、例えばとGCCの最近のバージョンを使用して、プログラムの中で、このようなエラーを検出することができます-Wallし、-Werrorして、GCCが完全にあなたのプログラムをコンパイルすることを拒否します。以下は、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);

明確に定義されており、i1 ずつ増加して古い値を生成し、その値を破棄します。次に、カンマ演算子で、副作用を解決します。そして、i1 ずつインクリメントし、結果の値は式の値になります。つまり、これは単なるj = (i += 2)「巧妙な」書き込み方法である、単なる工夫された方法です

i += 2;
j = i;

ただし、,関数の引数リストにはないコンマ演算子、および別個の引数の評価の間にはシーケンスポイントは存在しません。代わりに、それらの評価は相互に順序付けられていません。したがって、関数呼び出し

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

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


14

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

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

などはそのルールに違反します。この規格は、動作は未定義であり、特定されていないとも述べています。一部のコンパイラはこれらを検出して結果を生成しますが、これは標準ではありません。

ただし、2つのシーケンス変数間で2つの異なる変数をインクリメントできます。

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

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


もちろん、1つの式内の異なる変数には適用されません。もしそうなら、それは完全な設計の失敗です!2番目の例で必要なのは、ステートメントの終わりと次の始まりの間で両方をインクリメントすることだけです。これは保証されています。これは、これらすべての中心にあるシーケンスポイントの概念のためです。
underscore_d

11

/programming/29505280/incrementing-array-index-in-c誰かがのような文についての質問しました:

int k[] = {0,1,2,3,4,5,6,7,8,9,10};
int i = 0;
int num;
num = k[++i+k[++i]] + k[++i];
printf("%d", num);

これは7を出力します... OPは6を出力することを期待していました。

++i増分は、計算の残りの部分の前に全て完全に保証するものではありません。実際、コンパイラが異なれば、結果も異なります。あなたが提供した例では、最初の2は++i、その後の値は、実行k[]の最後そして、読んだ++i後、k[]

num = k[i+1]+k[i+2] + k[i+3];
i += 3

最新のコンパイラはこれを非常にうまく最適化します。実際、おそらくあなたが最初に書いたコードよりも良いでしょう(それがあなたが望んだ方法で機能していたとすれば)。


5

計算のこの種では何が起こるかについての良い説明は、文書で提供されn1188からISO W14サイト

アイデアを説明します。

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

前のシーケンスポイントと次のシーケンスポイントの間で、オブジェクトは、式の評価によって、格納された値を最大で1回変更する必要があります。さらに、以前の値は、格納される値を決定するためにのみ読み取られるものとします。

のような式のシーケンスポイントi=i++はbefore i=とafter i++です。

上記で引用した論文では、プログラムが小さなボックスで形成されていると理解できると説明されています。各ボックスには、2つの連続するシーケンスポイント間の命令が含まれています。i=i++完全な式を区切る2つのシーケンスポイントがある場合、シーケンスポイントは標準の付録Cで定義されます。このような式はexpression-statement、文法のバッカスナウア形式のエントリと構文的に同等です(文法は、標準の付録Aにあります)。

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

i=i++

として解釈することができます

tmp = i
i=i+1
i = tmp

またはとして

tmp = i
i = tmp
i=i+1

コードを解釈するこれらすべての形式i=i++が有効であり、両方が異なる回答を生成するため、動作は未定義です。

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

編集:

そのようなあいまいさを説明するための他の優れた情報源は、c-faqサイト(本としても発行さています)からのエントリ、つまりここここここです。


この回答はどのように既存の回答に新しく追加されましたか?また、の説明もこの回答i=i++とよく似ています
2017年

@haccks私は他の答えを読みませんでした。ISO 9899の公式サイトopen-std.org/jtc1/sc22/wg14/www/docs/n1188.pdf
alinsoar

5

あなたの質問はおそらく、「なぜこれらの構造は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の式の評価順序のすべての側面を決定するわけではありません。特に、1つの式内に複数の新しい値を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それを変更します。

それが答えです。作成する式では、各変数が最大1回変更されることを確認してください。変数が変更された場合、その変数の値を他の場所で使用しないでください。


3

その理由は、プログラムが未定義の動作をしているためです。C ++ 98標準に従って必要なシーケンスポイントがないため、問題は評価順序にあります(C ++ 11の用語に従って、演算の前後にシーケンスがありません)。

ただし、1つのコンパイラーに固執すると、関数呼び出しやポインターを追加しない限り、動作が永続的になるため、動作が煩雑になります。

  • したがって、最初のGCC:Nuwen MinGW 15 GCC 7.1を使用すると、次の結果が得られます。

    #include<stdio.h>
    int main(int argc, char ** argv)
    {
    int i = 0;
    i = i++ + ++i;
    printf("%d\n", i); // 2
    
    i = 1;
    i = (i++);
    printf("%d\n", i); //1
    
    volatile int u = 0;
    u = u++ + ++u;
    printf("%d\n", u); // 2
    
    u = 1;
    u = (u++);
    printf("%d\n", u); //1
    
    register int v = 0;
    v = v++ + ++v;
    printf("%d\n", v); //2

    }

GCCはどのように機能しますか?右辺(RHS)の左から右の順序でサブ式を評価し、値を左辺(LHS)に割り当てます。これがまさに、JavaとC#の動作と標準の定義方法です。(はい、JavaおよびC#の同等のソフトウェアで動作が定義されています)。これは、RHSステートメントの各サブ式を左から右の順序で1つずつ評価します。各サブ式:++ c(プリインクリメント)が最初に評価され、次に値cが操作に使用され、次にポストインクリメントc ++)が評価されます。

GCC C ++によると:演算子

GCC C ++では、演算子の優先順位により、個々の演算子が評価される順序が制御されます

GCCが理解する定義済み動作C ++の同等のコード:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int i = 0;
    //i = i++ + ++i;
    int r;
    r=i;
    i++;
    ++i;
    r+=i;
    i=r;
    printf("%d\n", i); // 2

    i = 1;
    //i = (i++);
    r=i;
    i++;
    i=r;
    printf("%d\n", i); // 1

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

    u = 1;
    //u = (u++);
    r=u;
    u++;
    u=r;
    printf("%d\n", u); // 1

    register int v = 0;
    //v = v++ + ++v;
    r=v;
    v++;
    ++v;
    r+=v;
    v=r;
    printf("%d\n", v); //2
}

次に、Visual Studioに移動します。Visual Studio 2015の場合:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int i = 0;
    i = i++ + ++i;
    printf("%d\n", i); // 3

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

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

    u = 1;
    u = (u++);
    printf("%d\n", u); // 2 

    register int v = 0;
    v = v++ + ++v;
    printf("%d\n", v); // 3 
}

Visual Studioのしくみ、別のアプローチ、最初のパスですべてのプリインクリメント式を評価し、2番目のパスで操作に変数値を使用し、3番目のパスでRHSからLHSに割り当て、最後にすべての評価を行うワンパスのインクリメント後の式。

したがって、Visual C ++が理解するのと同等の定義済み動作C ++:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int r;
    int i = 0;
    //i = i++ + ++i;
    ++i;
    r = i + i;
    i = r;
    i++;
    printf("%d\n", i); // 3

    i = 1;
    //i = (i++);
    r = i;
    i = r;
    i++;
    printf("%d\n", i); // 2 

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

    u = 1;
    //u = (u++);
    r = u;
    u = r;
    u++;
    printf("%d\n", u); // 2 

    register int v = 0;
    //v = v++ + ++v;
    ++v;
    r = v + v;
    v = r;
    v++;
    printf("%d\n", v); // 3 
}

Visual Studioのドキュメントに記載されている優先順位と評価の順序

複数の演算子が一緒に表示される場合、それらは同じ優先順位を持ち、結合性に従って評価されます。表の演算子については、Postfix演算子で始まるセクションで説明しています。


1
質問を編集して、関数の引数の評価にUBを追加しました。この質問は、その質問の複製としてよく使用されるためです。(最後の例)
Antti Haapala 2017年

1
また、質問は約あるC、C ++、今ではない
アンティHaapala
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.