web-dev-qa-db-ja.com

C ++関数呼び出しでインクリメント演算子を使用することは合法ですか?

この質問 では、次のコードが合法的なC++であるかどうかについていくつかの議論が行われています。

_std::list<item*>::iterator i = items.begin();
while (i != items.end())
{
    bool isActive = (*i)->update();
    if (!isActive)
    {
        items.erase(i++);  // *** Is this undefined behavior? ***
    }
    else
    {
        other_code_involving(*i);
        ++i;
    }
}
_

ここでの問題は、erase()が問題のイテレータを無効にすることです。 _i++_が評価される前にそれが発生した場合、そのようにiをインクリメントすることは、特定のコンパイラで機能しているように見えても、技術的には未定義の動作です。議論の一方は、関数が呼び出される前にすべての関数の引数が完全に評価されると述べています。反対側は、「唯一の保証は、i ++が次のステートメントの前とi ++の使用後に発生することです。それがerase(i ++)が呼び出される前か後かは、コンパイラーに依存します。」

私はこの質問を開いて、うまくいけばその議論を解決しました。

44

C++標準 1.9.16:

関数を呼び出すとき(関数がインラインであるかどうかに関係なく)、任意の引数式、または呼び出された関数を指定する後置式に関連付けられたすべての値の計算と副作用は、本体のすべての式またはステートメントの実行前にシーケンスされます。関数と呼ばれます。 (注:さまざまな引数式に関連する値の計算と副作用は順序付けられていません。)

したがって、このコードは次のように思われます。

foo(i++);

完全に合法です。 iをインクリメントしてから、以前の値fooiを呼び出します。ただし、このコード:

foo(i++, i++);

1.9.16項にも次のように記載されているため、未定義の動作が発生します。

スカラーオブジェクトの副作用が、同じスカラーオブジェクトの別の副作用、または同じスカラーオブジェクトの値を使用した値の計算に比べて順序付けされていない場合、動作は未定義です。

60

構築するには クリストの答え

foo(i++, i++);

関数の引数が評価される順序が未定義であるため、未定義の動作が発生します(より一般的なケースでは、変数を書き込む式で変数を2回読み取ると、結果は未定義になります)。どの引数が最初にインクリメントされるかはわかりません。

int i = 1;
foo(i++, i++);

の関数呼び出しが発生する可能性があります

foo(2, 1);

または

foo(1, 2);

あるいは

foo(1, 1);

以下を実行して、プラットフォームで何が起こるかを確認します。

#include <iostream>

using namespace std;

void foo(int a, int b)
{
    cout << "a: " << a << endl;
    cout << "b: " << b << endl;
}

int main()
{
    int i = 1;
    foo(i++, i++);
}

私のマシンでは

$ ./a.out
a: 2
b: 1

毎回ですが、このコードは移植性がないので、コンパイラが異なれば異なる結果が得られると思います。

14
Bill the Lizard

標準では、副作用は呼び出しの前に発生するとされているため、コードは次のようになります。

std::list<item*>::iterator i_before = i;

i = i_before + 1;

items.erase(i_before);

ではなく:

std::list<item*>::iterator i_before = i;

items.erase(i);

i = i_before + 1;

したがって、この場合は安全です。list.erase()は、消去されたイテレータ以外のイテレータを明確に無効にしないためです。

とはいえ、それは悪いスタイルです-すべてのコンテナの消去関数は特に次のイテレータを返すので、再割り当てのためにイテレータを無効にすることを心配する必要はありません。したがって、慣用的なコードは次のとおりです。

i = items.erase(i);

リストに対して安全であり、ストレージを変更したい場合は、ベクター、両端キュー、およびその他のシーケンスコンテナに対しても安全です。

また、警告なしに元のコードをコンパイルすることはできません-あなたは書く必要があります

(void)items.erase(i++);

未使用の返品に関する警告を回避するため。これは、何か奇妙なことをしていることの大きな手がかりになります。

5
Pete Kirkham

++クリスト!

C++標準1.9.16は、クラスのoperator ++(postfix)を実装する方法に関して非常に理にかなっています。そのoperator ++(int)メソッドが呼び出されると、それ自体がインクリメントされ、元の値のコピーが返されます。まさにC++仕様が言うように。

基準が改善されているのを見るのは素晴らしいことです!


ただし、古い(ANSI以前の)Cコンパイラを使用したことをはっきりと覚えています。

foo -> bar(i++) -> charlie(i++);

あなたが思うことをしませんでした!代わりに、次と同等にコンパイルされます。

foo -> bar(i) -> charlie(i); ++i; ++i;

そして、この動作はコンパイラの実装に依存していました。 (移植を楽しくする。)


最新のコンパイラが正しく動作するようになったことをテストおよび検証するのは簡単です。

#define SHOW(S,X)  cout << S << ":  " # X " = " << (X) << endl

struct Foo
{
  Foo & bar(const char * theString, int theI)
    { SHOW(theString, theI);   return *this; }
};

int
main()
{
  Foo f;
  int i = 0;
  f . bar("A",i) . bar("B",i++) . bar("C",i) . bar("D",i);
  SHOW("END ",i);
}


スレッド内のコメントに応答しています...

...そしてほとんどみんなのの答えに基づいて構築しています...(みんなありがとう!)


これをもう少し詳しく説明する必要があると思います。

与えられた:

baz(g(),h());

次に、g()が呼び出される前または後に= h()「未指定」です。

しかし、g()h()の両方がbaz()の前に呼び出されます。

与えられた:

bar(i++,i++);

繰り返しますが、どのi ++が最初に評価されるかはわかりません。また、bar()が呼び出される前にiが1回または2回インクリメントされるかどうかさえわかりません。 結果は未定義です!i = 0の場合、これはbar(0,0)またはbarになります(1,0)またはbar(0,1)または本当に奇妙な何か!)


与えられた:

foo(i++);

foo()が呼び出される前に、iがインクリメントされることがわかりました。 Kristo から指摘されたように C++標準セクション1.9.16:

関数を呼び出すとき(関数がインラインであるかどうかに関係なく)、任意の引数式、または呼び出された関数を指定する後置式に関連付けられたすべての値の計算と副作用は、本体のすべての式またはステートメントの実行前にシーケンスされます。関数と呼ばれます。 [注:異なる引数式に関連する値の計算と副作用は順序付けられていません。 -エンドノート]

セクション5.2.6の方が良いと思いますが、

後置++式の値は、そのオペランドの値です。 [注:取得された値は元の値のコピーです-エンドノート]オペランドは変更可能な左辺値でなければなりません。オペランドの型は、算術型または完全な実効オブジェクト型へのポインタでなければなりません。オペランドオブジェクトの値は、オブジェクトがブール型である場合を除き、1を加算することによって変更されます。ブール型の場合は、trueに設定されます。 [注:この使用は非推奨です。付録Dを参照してください。--end note] ++式の値の計算は、オペランドオブジェクトを変更する前にシーケンスされます。 不確定に順序付けられた関数呼び出しに関して、後置++の操作は単一の評価です。 [注:したがって、関数呼び出しは、左辺値から右辺値への変換と、単一の接尾辞++演算子に関連する副作用の間に介在してはなりません。 --end note]結果は右辺値です。結果の型は、オペランドの型のcv非修飾バージョンです。 5.7および5.17も参照してください。

セクション1.9.16の標準には、(例の一部として)次のものもリストされています。

i = 7, i++, i++;    // i becomes 9 (valid)
f(i = -1, i = -1);  // the behavior is undefined

そして、これを簡単に示すことができます。

#define SHOW(X)  cout << # X " = " << (X) << endl
int i = 0;  /* Yes, it's global! */
void foo(int theI) { SHOW(theI);  SHOW(i); }
int main() { foo(i++); }

したがって、はい、ifoo()が呼び出される前にインクリメントされます。


これはすべて、次の観点から非常に理にかなっています。

class Foo
{
public:
  Foo operator++(int) {...}  /* Postfix variant */
}

int main() {  Foo f;  delta( f++ ); }

ここでFoo :: operator ++(int)delta()の前に呼び出す必要があります。そして、インクリメント操作はその呼び出し中に完了する必要があります。


私の(おそらく過度に複雑な)例では:

f . bar("A",i) . bar("B",i++) . bar("C",i) . bar("D",i);

f.bar( "A"、i)は、object.bar( "B"、i ++)に使用されるオブジェクトを取得するために実行する必要があり、以下同様に"C"および「D」

したがって、i ++は、bar( "B"、i ++)を呼び出す前にiをインクリメントします(bar( "B"、...)はで呼び出されますがi)の古い値、したがってibar( "C"、i)およびbar( "D"、i)の前にインクリメントされます。


j_random_hackerのコメントに戻る:

j_random_hackerの書き込み:
+ 1ですが、これで問題ないことを確信するために、標準を注意深く読む必要がありました。 bar()が代わりにsay intを返すグローバル関数であり、fがintであり、それらの呼び出しが "。"ではなくsay "^"で接続されている場合、A、C、Dのいずれかが可能であると私は考えていますか?レポート「0」?

この質問は、あなたが思っているよりもはるかに複雑です...

質問をコードとして書き直します...

int bar(const char * theString, int theI) { SHOW(...);  return i; }

bar("A",i)   ^   bar("B",i++)   ^   bar("C",i)   ^   bar("D",i);

これで、[〜#〜] 1つ[〜#〜]の式しかありません。標準によると(セクション1.9、8ページ、pdfページ20):

注:演算子は、演算子が実際に結合法則または可換法則である場合にのみ、通常の数学的規則に従って再グループ化できます。(7)たとえば、次のフラグメントでは、a = a + 32760 + b + 5;式ステートメントは、次とまったく同じように動作します。a=(((a + 32760)+ b)+5);これらの演算子の結合性と優先順位のため。したがって、合計の結果(a + 32760)が次にbに加算され、次にその結果が5に加算されて、値がaに割り当てられます。オーバーフローによって例外が発生し、intで表現できる値の範囲が[-32768、+ 32767]であるマシンでは、実装はこの式をa =((a + b)+32765);として書き換えることはできません。 aとbの値がそれぞれ-32754と-15の場合、合計a + bは例外を生成しますが、元の式は例外を生成しません。また、式をa =((a + 32765)+ b);として書き換えることもできません。またはa =(a +(b + 32765)); aとbの値は、それぞれ4と-8、または-17と12であった可能性があるためです。ただし、オーバーフローが例外を生成せず、の結果がオーバーフローは元に戻すことができます。同じ結果が発生するため、上記の式ステートメントは、上記のいずれかの方法で実装によって書き換えることができます。 -エンドノート]

したがって、優先順位により、式は次のようになると考えるかもしれません。

(
       (
              ( bar("A",i) ^ bar("B",i++)
              )
          ^  bar("C",i)
       )
    ^ bar("D",i)
);

ただし、(a ^ b)^ c == a ^(b ^ c)はオーバーフローの可能性がないため、任意の順序で書き換えることができます...

ただし、bar()が呼び出されており、仮想的に副作用が発生する可能性があるため、この式を任意の順序で書き換えることはできません。優先規則は引き続き適用されます。

これは、bar()のの評価の順序を適切に決定します。

さて、それはいつi + = 1発生しますか?bar( "B"、...)が呼び出される前に発生する必要があります。 (bar( "B"、....)は古い値で呼び出されますが。)

したがって、bar(C)およびbar(D)の前、およびbar(A)の後に決定論的に発生します。

回答:いいえ。コンパイラが標準に準拠している場合は、常に「A = 0、B = 0、C = 1、D = 1」になります。


しかし、別の問題を考えてみましょう。

i = 0;
int & j = i;
R = i ^ i++ ^ j;

Rの値は何ですか?

i + = 1jの前に発生した場合、0 ^ 0 ^ 1 = 1になります。ただし、式全体の後にi + = 1が発生した場合、0 ^ 0 ^ 0 = 0になります。

実際、Rはゼロです。i + = 1は、式が評価されるまで発生しません。


私が考える理由は次のとおりです。

i = 7、i ++、i ++; //私は9になります(有効)

合法です... 3つの表現があります。

  • i = 7
  • i ++
  • i ++

そして、いずれの場合も、iの値は、各式の終了時に変更されます。 (後続の式が評価される前。)


PS:検討してください:

int foo(int theI) { SHOW(theI);  SHOW(i);  return theI; }
i = 0;
int & j = i;
R = i ^ i++ ^ foo(j);

この場合、i + = 1foo(j)の前に評価する必要があります。theIは1です。そして[〜#〜] r [〜#〜]は0 ^ 0 ^ 1 = 1です。

3
Mr.Ree

それは完全に大丈夫です。渡される値は、インクリメント前の「i」の値になります。

3

MarkusQの答えに基づいて構築するには:;)

というか、ビルのコメント:

編集:ああ、コメントはまた消えた...まあ)

それらは並行して評価されることが許可されています。それが実際に起こるかどうかは、技術的には無関係です。

ただし、これを行うためにスレッドの並列処理は必要ありません。両方の最初のステップを評価し(iの値を取得)、2番目のステップを評価します(iをインクリメントします)。完全に合法であり、一部のコンパイラは、2番目のi ++を開始する前に1つのi ++を完全に評価するよりも効率的であると考える場合があります。

実際、私はそれが一般的な最適化であると期待しています。命令スケジューリングの観点から見てください。評価する必要があるものは次のとおりです。

  1. 正しい引数としてiの値を取ります
  2. 正しい引数でiをインクリメントします
  3. 左の引数にiの値を取ります
  4. 左の引数でiをインクリメントします

しかし、実際には、左と右の引数の間に依存関係はありません。引数の評価は不特定の順序で行われ、順番に実行する必要もありません(そのため、関数の引数のnew()は、スマートポインターでラップされている場合でも、通常はメモリリークになります)同じ変数を変更したときに何が起こるかも未定義です同じ式で2回。ただし、1と2の間、および3と4の間の依存関係があります。では、なぜコンパイラは2が完了するのを待ってから3を計算するのでしょうか。これによりレイテンシが追加され、4が利用可能になるまでに必要以上に時間がかかります。それぞれの間に1サイクルのレイテンシがあると仮定すると、1が完了してから4の結果の準備が整い、関数を呼び出すことができるようになるまで、3サイクルかかります。

しかし、それらを並べ替えて1、3、2、4の順序で評価すると、2サイクルで実行できます。 1と3は同じサイクルで開始でき(または同じ式であるため、1つの命令にマージすることもできます)、以下では2と4を評価できます。最新のCPUはすべて、1サイクルあたり3〜4命令を実行できます。優れたコンパイラは、それを利用しようとする必要があります。

1
jalf

Sutter's Guru of the Week#55 (および「MoreExceptional C++」の対応する部分)では、この正確なケースを例として説明しています。

彼によると、これは完全に有効なコードであり、実際、ステートメントを2行に変換しようとする場合です。

 items.erase(i); 
 i ++; 

しますか ない 元のステートメントと意味的に同等のコードを生成します。

0
Boojum