9

私はイントロC++クラスのTAです。先週のテストで次の質問がありました。

次のプログラムからの出力は何ですか。

int myFunc(int &x) {
   int temp = x * x * x;
   x += 1;
   return temp;
}

int main() {
   int x = 2;
   cout << myFunc(x) << endl << myFunc(x) << endl << myFunc(x) << endl;
}

私と私のすべての同僚にとっての答えは、明らかに次のとおりです。

8
27
64

しかし今、何人かの学生は、特定の環境でこれを実行すると、実際には反対になると指摘しています。

64
27
8

gccを使用してLinux環境で実行すると、期待どおりの結果が得られます。私のWindowsマシンでMinGWを使用すると、彼らが話していることがわかります。myFuncへの最後の呼び出しを最初に評価し、次に2番目の呼び出し、次に最初の呼び出しを評価しているようです。すべての結果が得られると、最初の呼び出しから通常の順序で出力されます。しかし、呼び出しが順不同で行われたため、番号は反対です。

私はコンパイラの最適化であり、関数呼び出しを逆の順序で評価することを選択しているように見えますが、その理由はよくわかりません。私の質問は:私の仮定は正しいですか?それはバックグラウンドで何が起こっているのですか?それともまったく違うものがありますか?また、関数を逆方向に評価してから出力を順方向に評価することに利点がある理由がよくわかりません。ostreamの動作方法のために出力は順方向である必要がありますが、関数の評価も順方向である必要があるようです。

ご協力いただきありがとうございます!

4

6 に答える 6

15

C ++標準では、完全な式の部分式が評価される順序は定義されていません。ただし、順序を導入する特定の演算子(コンマ演算子、3項演算子、論理演算子の短絡)と、関数/演算子の引数/演算子はすべて、関数/演算子自体の前に評価されます。

GCCは、なぜ注文したいのかをあなた(または私)に説明する義務はありません。これはパフォーマンスの最適化である可能性があります。コンパイラコードが数行短く単純になっているためである可能性があります。これは、mingwコーダーの1人が個人的にあなたを嫌っており、そうでない仮定をした場合にそれを確認したいためである可能性があります。標準で保証されているので、コードが間違っています。オープンスタンダードの世界へようこそ:-)

編集して追加:litbは、(未)定義された動作について以下の点を指摘します。標準では、式で変数を複数回変更し、その式に有効な評価順序が存在する場合、変数が間にシーケンスポイントなしで複数回変更される場合、式の動作は未定義であるとされています。変数は関数の呼び出しで変更され、関数呼び出しの開始時にシーケンスポイントがあるため(コンパイラがインライン化した場合でも)、これはここでは当てはまりません。ただし、手動でコードをインライン化した場合は、次のようになります。

std::cout << pow(x++,3) << endl << pow(x++,3) << endl << pow(x++,3) << endl;

その場合、それは未定義の動作になります。このコードでは、コンパイラが3つの「x ++」部分式すべてを評価し、次に3つのpowの呼び出しを評価してから、のさまざまな呼び出しを開始することが有効ですoperator<<。この順序は有効であり、xの変更を分離するシーケンスポイントがないため、結果は完全に未定義です。コードスニペットでは、実行の順序のみが指定されていません。

于 2009-10-01T15:53:11.107 に答える
10

正確に、これが不特定の動作をするのはなぜですか。

この例を最初に見たとき、この式は実際には一連の関数呼び出しの省略形であるため、動作が明確に定義されていると感じました。

このより基本的な例を考えてみましょう。

cout << f1() << f2();

これは一連の関数呼び出しに拡張され、呼び出しの種類は、メンバーまたは非メンバーである演算子によって異なります。

// Option 1:  Both are members
cout.operator<<(f1 ()).operator<< (f2 ());

// Option 2: Both are non members
operator<< ( operator<<(cout, f1 ()), f2 () );

// Option 3: First is a member, second non-member
operator<< ( cout.operator<<(f1 ()), f2 () );

// Option 4: First is a non-member, second is a member
cout.operator<<(f1 ()).operator<< (f2 ());

最も低いレベルでは、これらはほぼ同じコードを生成するため、これからは最初のオプションのみを参照します。

標準では、関数の本体を入力する前に、コンパイラが各関数呼び出しの引数を評価する必要があることが保証されていますこの場合、の結果は他の演算子を呼び出すために必要であるため、isのcout.operator<<(f1())前に評価する必要があります。operator<<(f2())cout.operator<<(f1())

演算子への呼び出しは順序付けする必要がありますが、引数にそのような要件がないため、不特定の動作が始まります。したがって、結果の順序は次のいずれかになります。

f2()
f1()
cout.operator<<(f1())
cout.operator<<(f1()).operator<<(f2());

または:

f1()
f2()
cout.operator<<(f1())
cout.operator<<(f1()).operator<<(f2());

または最後に:

f1()
cout.operator<<(f1())
f2()
cout.operator<<(f1()).operator<<(f2());
于 2009-10-01T17:13:37.443 に答える
2

関数呼び出しパラメーターが評価される順序は指定されていません。つまり、ステートメントの意味と結果に影響を与える副作用のある引数を使用しないでください。

于 2009-10-01T15:53:23.883 に答える
0

ええ、機能引数の評価の順序は、標準に従って「指定されていません」です。

したがって、出力はプラットフォームによって異なります

于 2009-10-01T15:53:06.287 に答える
0

すでに述べたように、あなたは未定義の行動の幽霊の森に迷い込んだ。毎回期待されるものを取得するには、副作用を取り除くことができます。

int myFunc(int &x) {
   int temp = x * x * x;
   return temp;
}

int main() {
   int x = 2;
   cout << myFunc(x) << endl << myFunc(x+1) << endl << myFunc(x+2) << endl;
   //Note that you can't use the increment operator (++) here.  It has
   //side-effects so it will have the same problem
}

または、関数呼び出しを個別のステートメントに分割します。

int myFunc(int &x) {
   int temp = x * x * x;
   x += 1;
   return temp;
}

int main() {
   int x = 2;
   cout << myFunc(x) << endl;
   cout << myFunc(x) << endl;
   cout << myFunc(x) << endl;
}

2番目のバージョンは、副作用を考慮するように強制されるため、テストにはおそらく適しています。

于 2009-10-01T16:01:48.233 に答える
0

そしてこれが、副作用のある関数を書くたびに、神が子猫を殺す理由です!

于 2009-10-01T17:49:30.707 に答える