cout << a ++ << a;の正しい答えは何ですか?


98

最近のインタビューで、次の客観的なタイプの質問がありました。

int a = 0;
cout << a++ << a;

答え:

a。10
b。01
c。未定義の動作

選択肢bに答えました。つまり、出力は「01」になります。

しかし、驚いたことに、インタビュアーから正しい答えはオプションc:未定義であると言われました。

今、私はC ++のシーケンスポイントの概念を知っています。次のステートメントの動作は未定義です。

int i = 0;
i += i++ + i++;

しかし、文の私の理解あたりとしてcout << a++ << aostream.operator<<()最初で、二回呼び出されますostream.operator<<(a++)以降ostream.operator<<(a)

VS2010コンパイラで結果を確認したところ、その出力も「01」です。


30
説明を求めましたか?潜在的な候補者を面接することが多く、質問を受けることに非常に興味があります。
ブレイディ

3
@jrok未定義の動作です。実装が行うこと(上司にあなたの名前で侮辱的な電子メールを送信することを含む)はすべて適合しています。
James Kanze、2012年

2
この質問は、シーケンスポイントについて言及していないC ++ 11(現在のバージョンのC ++)の回答を求めています。残念ながら、C ++ 11でのシーケンスポイントの置換については、十分な知識がありません。
CBベイリー

3
それができませんでした間違いなくそれを未定義されなかった場合10、それはどちらかだろう0100。(c++常に値と評価されますc持っていた前に、インクリメントされます)。そして、それが未定義ではなかったとしても、それは恐ろしく混乱させるでしょう。
leftaround約

2
「cout << c ++ << c」というタイトルを読んだとき、それをC言語とC ++言語、および「cout」という名前の別の言語との関係についての声明だと思ったことがあります。「裁判所未満は」非常に、だったことと、おそらく推移で-あなたは、誰かが、彼らは「裁判所未満は」C ++にかなり劣っていたと考えどのように言っていたような、そしてC ++はCにかなり劣っていたことを、知って非常に :) C.にずっと劣っ
tchrist

回答:


145

あなたは考えることができます:

cout << a++ << a;

なので:

std::operator<<(std::operator<<(std::cout, a++), a);

C ++は、以前の評価のすべての副作用がシーケンスポイントで実行されることを保証します。関数の引数の評価の間にシーケンスポイントはありません。つまり、引数aは引数の前std::operator<<(std::cout, a++)または後に評価できます。したがって、上記の結果は未定義です。


C ++ 17アップデート

C ++ 17では、ルールが更新されました。特に:

シフト演算子式E1<<E2and E1>>E2では、すべての値の計算と副作用が、すべての値の計算と副作用のE1前に順序付けられますE2

つまり、b出力を生成する結果を生成するコードが必要01です。

詳細については、P0145R3慣用的なC ++の式評価順序の調整」を参照してください。


@Maxim:解説をありがとう。調べた呼び出しでは、動作は未定義になります。しかし、今、私はもう1つの質問があります(もっとおかしい質問かもしれませんが、基本的なものや大声で考えているものが欠けています)。 <()メンバーのバージョン。デバッグでは、グローバルバージョンではなくostream :: operator <<()呼び出しのメンバーバージョンに着陸しています。これが、最初は答えが01だと思った理由です
pravs

@Maxim違いはありませんが、ctype があるためintoperator<<ここにメンバー関数があります。
James Kanze、2012年

2
@pravs:operator<<メンバー関数か独立関数かはシーケンスポイントに影響しません。
Maxim Egorushkin、2012年

11
「シーケンスポイント」は、C ++標準では使用されなくなりました。それは不正確であり、「前にシーケンスされた/後にシーケンスされた」関係に置き換えられました。
ラファウDowgird

2
So the result of the above is undefined.あなたの説明は不特定のためだけに有効であり、未定義のためではありません。JamesKanzeはそれがより多くの破滅はどのように説明未定義 けれども彼の答えに
Deduplicator 2014年

68

技術的には、これは全体として未定義の動作です。

しかし、答えには2つの重要な側面があります。

コードステートメント:

std::cout << a++ << a;

次のように評価されます。

std::operator<<(std::operator<<(std::cout, a++), a);

この規格では、関数の引数の評価順序は定義されていません。
だからどちらか:

  • std::operator<<(std::cout, a++) 最初に評価されるか
  • a最初に評価されるか
  • これは、実装で定義された順序になる可能性があります。

この順序は、規格によれば、未指定です[参照1]

[参照1] C ++ 03 5.2.2関数呼び出し
Para 8

引数の評価順序は不定です。引数式評価のすべての副作用は、関数に入る前に有効になります。後置式と引数式リストの評価の順序は指定されていません。

さらに、関数の引数の評価の間にシーケンスポイントはありませんが、シーケンスポイントはすべての引数の評価後にのみ存在します[参照2]

[参照2] C ++ 03 1.9プログラムの実行[intro.execution]:
パラ17:

(関数がインラインであるかどうかに関係なく)関数を呼び出す場合、すべての関数引数(ある場合)の評価後、関数本体の式またはステートメントの実行前に発生するシーケンスポイントがあります。

ここで、の値はc、シーケンスポイントを介さずに複数回アクセスされていることに注意してください。これに関して、標準では次のように述べています。

[参照3] C ++ 03 5式[式]:
パラ4:

....
前のシーケンスポイントと次のシーケンスポイントの間で、スカラーオブジェクトの保存された値は、式の評価によって最大で1回変更されます。さらに、以前の値は、保存される値を決定するためにのみアクセスされるものとします。このパラグラフの要件は、完全な式の部分式の許容可能な順序ごとに満たされるものとします。それ以外の場合の動作は未定義です。

コードはcシーケンスポイントを介さずに複数回変更され、格納されたオブジェクトの値を決定するためにアクセスされません。これは明らかに上記の条項に違反しているため、標準で義務付けられている結果はUndefined Behavior [参照3]です。


1
技術的には、オブジェクトの変更があり、シーケンスポイントを介さずに他の場所にアクセスするため、動作は未定義です。未定義は指定ではありません。実装にはさらに余裕があります。
James Kanze、2012年

1
@Alsはい。私はあなたの編集を見ていませんでした(ただし、プログラムは奇妙なことはできないというjrokの発言に反応していました---それはできます)。あなたの編集したバージョンはそれでいいですが、私の心のキーワードは部分的な順序付けです。シーケンスポイントは、部分的な順序付けのみを導入します。
James Kanze、2012年

1
@詳細な説明をありがとう、本当にとても役に立ちました!!
pravs

4
新しいC ++ 0x標準は本質的に同じですが、セクションが異なり、表現が異なります:)引用:(1.9プログラムの実行[intro.execution]、パラメーター15):「スカラーオブジェクトへの副作用がシーケンスされていない場合同じスカラーオブジェクトに対する別の副作用、または同じスカラーオブジェクトの値を使用した値の計算のいずれかである場合、動作は未定義です。」
ラファウDowgird

2
この回答にはバグがあると思います。"std :: cout << c ++ << c;" std :: operator <<(std :: ostream&、int)が存在しないため、「std :: operator <<(std :: operator <<(std :: cout、c ++)、c)」に変換できません。代わりに、「std :: cout.operator <<(c ++)。operator(c);」に変換されます。実際には、「c ++」と「c」の評価の間にシーケンスポイントがあります(オーバーロードされた演算子は、関数呼び出し、したがって、関数呼び出しが戻るときにシーケンスポイントがあります)。その結果、動作と実行順序指定されます。
Christopher Smith

20

シーケンスポイントは、部分的な順序付けのみを定義します。あなたの場合、あなたは持っています(一度過負荷解決が行われると):

std::cout.operator<<( a++ ).operator<<( a );

間の配列の点があるa++との最初の呼び出しは std::ostream::operator<<、第二の配列との間の点があるaとの第二の呼び出しはstd::ostream::operator<<、ない配列点の間存在しないa++a、唯一の順序付けの制約はa++、最初の呼び出しの前に完全に評価され(副作用を含む)operator<<、2番目の制約はa2番目の呼び出しの前に完全に評価されることoperator<<です。(因果的な順序の制約もありoperator<<ます。最初の結果を引数として必要とするため、2番目の呼び出しは最初の呼び出しに優先できません。)§5/ 4(C ++ 03)は次のように述べています。

特に明記されていない限り、個々の演算子のオペランドと個々の式の部分式の評価の順序、および副作用が発生する順序は指定されていません。前のシーケンスポイントと次のシーケンスポイントの間で、スカラーオブジェクトは、式の評価によって、格納された値を最大で1回変更します。さらに、以前の値は、保存される値を決定するためにのみアクセスされるものとします。この段落の要件は、完全な式の部分式の許容される順序ごとに満たされるものとします。それ以外の場合の動作は未定義です。

あなたの表現の許容順序の一つであるa++a、の最初の呼び出しoperator<<に、2回目の呼び出しoperator<<。これはaa++)の格納された値を変更し、新しい値(2番目のa)を決定する以外の方法でアクセスします。動作は未定義です。


標準の引用からの1つのキャッチ。「注記がある場合を除いて」IIRCには、オーバーロードされた演算子を処理するときの例外が含まれます。これは、演算子を関数として扱い、std :: ostream :: operator <<(int )。私が間違っていたら訂正してください。
クリストファースミス

@ChristopherSmithオーバーロードされた演算子は、関数呼び出しのように動作します。の代わりにcユーザーが定義されたユーザータイプの場合、結果は指定されませんが、未定義の動作はありません。++int
James Kanze 2013

1
あなたは2間のシーケンスポイントご覧ください@ChristopherSmith cではfoo(foo(bar(c)), c)?関数が呼び出されるときと関数が返るときのシーケンスポイントがありますが、2つのの評価の間に関数呼び出しは必要ありませんc
James Kanze、2014年

1
@ChristopherSmith cがUDTの場合、オーバーロードされた演算子関数呼び出しであり、シーケンスポイントが導入されるため、動作は未定義ではありません。ただし、部分式cがの前に評価された後か、その後に評価されたかはまだ不明c++であるため、増分バージョンを取得するかどうかは指定されません(理論的には、毎回同じである必要はありません)。
James

1
@ChristopherSmithシーケンスポイントの前のすべてがシーケンスポイントの後の前に発生します。ただし、シーケンスポイントは部分的な順序付けのみを定義します。たとえば、問題の式では、部分式cとの間にシーケンスポイントがないc++ため、2つは任意の順序で出現する可能性があります。セミコロンについては...完全な式である限り、シーケンスポイントを発生させます。その他の重要なシーケンスポイントは、関数呼び出しです:f(c++)インクリメントでしょうcf、とコンマ演算子、&&||?:もシーケンスポイントが発生します。
James Kanze、2015年

4

正解は質問に質問することです。読者が明確な答えを見ることができないため、この発言は受け入れられません。別の見方をすると、ステートメントの解釈をはるかに困難にする副作用(c ++)が導入されています。簡潔なコードは、その意味が明確であれば、すばらしいものです。


4
質問は、プログラミングの実践が不十分であることを示している可能性があります(さらには無効なC ++)。しかし、答えは何が間違っているのか、なぜ間違っているのかを示す質問に答えるものです。質問の解説は完全に有効であっても回答にはなりません。せいぜい、これは回答ではなくコメントにすることができます。
PP
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.