web-dev-qa-db-ja.com

<random>を使用したC ++での乱数の順序

より大きなプログラムの一部をテストするために書いた次のコードがあります。

#include <fstream>
#include <random>
#include <iostream>
using namespace std ;

int main()
{
  mt19937_64 Generator(12187) ;
  mt19937_64 Generator2(12187) ;
  uniform_int_distribution<int> D1(1,6) ;

  cout << D1(Generator) << " " ;
  cout << D1(Generator) << " " << D1(Generator) << endl ;
  cout << D1(Generator2) << " " << D1(Generator2) << " " << D1(Generator2) << endl ;

  ofstream g1("g1.dat") ;
  g1 << Generator ;
  g1.close() ;
  ofstream g2("g2.dat") ;
  g2 << Generator2 ;
  g2.close() ;
}                                                            

2つのジェネレーターには同じ値がシードされているため、出力の2番目の行は最初の行と同じであると予想しました。代わりに、出力は

1 1 3
1 3 1

*.datファイルに出力された2つのジェネレーターの状態は同じです。乱数生成に隠れたマルチスレッドがあり、順序の不一致が発生するのではないかと考えていました。

Linuxではg++バージョン5.3.0を使用して、フラグ-std=c++11を付けてコンパイルしました。

よろしくお願いします。

39

_x << y_は、operator<<(x, y)への関数呼び出しの構文糖衣です。

C++標準では、関数呼び出しの引数の評価順序に制限がないことを思い出してください。

したがって、コンパイラーは、xを最初にまたはyを最初に計算するコードを自由に出力できます。

標準から:§5注2:

演算子はオーバーロードできます。つまり、クラス型(第9節)または列挙型(7.2)の式に適用すると意味が与えられます。 オーバーロードされた演算子の使用は、13.5で説明されているように関数呼び出しに変換されます。オーバーロードされた演算子は、第5項で指定された構文の規則に従いますが、オペランドタイプ、値のカテゴリ、および評価順序は関数呼び出しの規則に置き換えられますの要件に従います。

42
Richard Hodges

これは、この行の評価の順序が

cout << D1(Generator2) << " " << D1(Generator2) << " " << D1(Generator2) << endl ;

あなたが思うことではありません。

あなたはこれでそれをテストすることができます:

int f() {
  static int i = 0;
  return i++;
}

int main() {
  cout << f() << " " << f() << " " << f() << endl ;
  return 0;
}

出力:2 1 0


順序はC++標準で指定されていないため、他のコンパイラでは順序が異なる可能性があります。RichardHodgesの回答を参照してください。

36
alain

プログラムを少し変更すると、何が起こるかがわかります。

#include <fstream>
#include <random>
#include <iostream>
using namespace std ;

int main()
{
  mt19937_64 Generator(12187) ;
  mt19937_64 Generator2(12187) ;
  uniform_int_distribution<int> D1(1,100) ;

  cout << D1(Generator) << " " ;
  cout << D1(Generator) << " " ;
  cout << D1(Generator) << endl ;
  cout << D1(Generator2) << " " << D1(Generator2) << " " << D1(Generator2) << endl ;
}

出力:

4 48 12
12 48 4

したがって、ジェネレータは同じ結果を生成しますが、cout-lineの引数の順序は異なる順序で計算されます。

オンラインでお試しください: http://ideone.com/rsoqDe

5
Anedar

これらの行

  cout << D1(Generator) << " " ;

  cout << D1(Generator) << " "
       << D1(Generator) << endl ;

  cout << D1(Generator2) << " "
       << D1(Generator2) << " "
       << D1(Generator2) << endl ;

D1()はintを返すため、ostream::operator<<()にはオーバーロードがあり、効果的に呼び出しています(endlを除く)

cout.operator<<(D1(Generator));

cout.operator<<(D1(Generator))
    .operator<<(D1(Generator));

cout.operator<<(D1(Generator2))
    .operator<<(D1(Generator2))
    .operator<<(D1(Generator2));

さて、標準にはこう言われています、

§5.2.2[4]

関数が呼び出されると、各パラメーターは対応する引数で初期化されます。

[注:このような初期化は、相互に不確定に順序付けられます—エンドノート]

関数が非静的メンバー関数である場合、関数のこのパラメーターは、呼び出しのオブジェクトへのポインターで初期化されます。

それでは、前の式を分解してみましょう

cout.operator<<(a())  // #1
    .operator<<(b())  // #2
    .operator<<(c()); // #3

thisポインターの構成を説明するために、これらは概念的には(簡潔にするためにostream::を省略)と同等です。

operator<<(           // #1
  &operator<<(        // #2
    &operator<<(      // #3
      &cout,
      a()
    ),                // end #3
    b()
  ),                  // end #2
  c()
);                    // end #1

それでは、トップレベルの呼び出しを見てみましょう。 #2c()のどちらを最初に評価しますか?引用で強調されているように、順序は不確定であるため、わかりません。これは再帰的に当てはまります。#2を評価したとしても、内部の#3を評価するかb()

うまくいけば、ここで何が起こっているのかがより明確に説明されます。

4
Yam Marcovic