C++11の新しい乱数の使い方おぼえがき


忘れた頃にふと必要になるので自分用にメモしておきます。

Taken by Matteo.Mazzoni, Flickr

Taken by Matteo.Mazzoni, Flickr

C++11には新しい乱数ライブラリが標準で用意されました。

もちろん従来の乱数rand()も使えますが、内部の実装が決まっていないのと、またその実装について様々な問題点が指摘されていることもあり、C++11ではより新しく使いやすい乱数が定義されたのでした。

これが int rand2(); みたいに定義されていれば使う側としては簡単なのですが、残念ながら使うにはもう少し長い構文が必要になります。

(何も考えず乱数を使いたい人にはかなりマニアックな話題だと思います。C++11の実装の話と乱数の話を行ったり来たりします。)

結論だけ書くと、以下のように乱数myrandを作ることができます。

#include <random>
#include <functional>
#include <ctime>

auto myrand = std::bind(std::uniform_int_distribution<int>(0, 99), std::mt19937(static_cast<unsigned int>(time(nullptr)));

例によってコンパイル時に-std=c++11 オプションをつけて下さい。


古いrandの限界

昔のrand()は<cstdlib>あるいは<stdlib.h>で定義されています。実装は場合によりますが、線形合同法だったり線形帰還シフトレジスタだったりするようです。(glibc 2.17の実装を見たところ線形帰還シフトレジスタが使われているようです。)

従来の乱数の問題点は、ひとつは規則正しさの度合いが結構強いことです。例えば普通の(オーバーフローを利用した)線形合同法による実装では、出てくる乱数が偶数と奇数が必ずどちらか、あるいは常に交互に出現してしまうようです。

もうひとつの問題点が乱数を用意した後の扱いの貧弱さです。例えばよくある実装ではrand()の最大値は「2147483647」という、十進法世界の住人にはいかにも微妙な値になっています。この生の乱数にはたいていの場合は用がありません。

よく「0から99までの乱数が欲しい」などというときに、簡単のため rand()%100 みたいな書き方をしますが、実際は最大値を100で割りきれないため正確に均等に分布してくれません

これは適当なコードを書いて試してみればすぐわかります。例えば0から2000000000-1までの範囲の乱数を作ろうとしたとします。上の例に従って乱数を作って、試しに回数を数えてみましょう。

#include <iostream>
#include <cstdlib>

int main(int argc, char** argv){
  srand(static_cast<unsigned int>(time(NULL)));
  std::cout << "RAND_MAX=" << RAND_MAX << std::endl;
  int count = 0;
  int r0 = 0, r1 = 0;
  int thresh = 100000000;
  int max = 2000000000;
  while(true){
    int r = rand()%max;
    if(r < thresh){
      r0++;
      count++;
      //std::cout << "0";
    }else if(r >= max-thresh){
      r1++;
      count++;
      //std::cout << "1";
    }
    if(count > 10000) break;
  }
  std::cout << "r0=" << r0 << ", r1=" << r1 << std::endl;
  return 0;
}

この場合だと、値切り捨ての関係で0付近の値と末尾の2000000000付近の値で出やすさが倍も変わります。手元の環境では”r0=6660, r1=3341″という結果が出ました。ちょうど倍ですね。


新しい乱数

C++11では新しいライブラリ<random>で乱数生成器が定義されています。種類はいろいろありますが、とりあえずmt19937(メルセンヌ・ツイスタ)を使っておけばおおよそ大丈夫です。十分高速で均等で周期がとても長いことがわかっています。

比較的新しいgccでも、clangでももちろん使えます。コンパイル時に-std=c++11 オプションは必要です。

以下のように使えます。

#include <random>

int main(int argc, char** argv){
  std::mt19937 rand2;
  int r = rand2();
  //do something
  return 0;
}

このままだと実行するたびに同じ値になりますね。srandに相当する構文は、作成時に適当に引数を与えてやる形で大丈夫です。定番は現在時刻ですね。

unsigned int型が必要なので、一応static_castでキャストしてあげるとすっきりします。

#include <random>
#include <ctime>

int main(int argc, char** argv){
  std::mt19937 rand2(static_cast<unsigned int>(time(nullptr)));
  int r = rand2();
  //do something
  return 0;
}

計算機のランダムノイズを種に作る方法もありますが、今回は省略します。(time(nullptr)の代わりにrandom_device()を使います。)

 

さて、ここで先程も考えた「ある範囲に均等に分布する乱数」を作る問題を考えます。<random>にはこれを解決するための専用の関数があります。

それがuniform_int_distributionです。1から6の範囲で欲しい時は以下のように使います。

#include <random>
#include <ctime>

int main(int argc, char** argv){
  std::mt19937 rand2(static_cast<unsigned int>(time(nullptr)));
  std::uniform_int_distribution<int> dist(1, 6);
  int r = dist(rand2);
  //do something
  return 0;
}

生成時に型(<int>という文)が必要だったり、使うときにいちいち乱数生成器が必要だったりと随分潔癖症な関数です。

実数用のunifrom_real_distribution<double> もよく使います。

(他にも正規分布など様々な分布を表現するためのものが数多く用意されていますが、一般的にはそこまで頼らなくてもどんな分布でも累積分布関数からすぐ再現することができますし、まともな形をした分布ならモンテカルロ法でも表現できます。今回はこの話は割愛します。)


funtionalライブラリとの結合

こうなるといちいち dist(rand2)とか書くのが面倒なので、「dist(rand2)をしてくれる関数dice()」が欲しくなります。これにはやはりC++11の新機能bindを使えば実現します。

bindは「ある関数の引数に特定の値(や関数)を引数にとった結果を返すような新しい関数」を作るものです。言い換えると引数の数を減らしてくれます。さらに返す型がやっかいなのでauto型推論を使って考えないことにします。以下のように使います。

#include <random>
#include <ctime>
#include <functional>

int main(int argc, char** argv){
  std::mt19937 rand2(static_cast<unsigned int>(time(nullptr)));
  std::uniform_int_distribution<int> dist(1, 6);
  auto dice = std::bind(dist, rand);
  int r = dice();
  //do something
  return 0;
}

生成器randとか分布distとかには名前をつける必要もないので、以下のように束ねてしまうことができます。

#include <random>
#include <ctime>
#include <functional>

int main(int argc, char** argv){
  auto dice = std::bind(std::uniform_int_distribution<int>(1,6), std::mt19937(static_cast<unsigned int>(timer)));
  int r = dice();
  //do something
  return 0;
}

…というわけで、やっと目的の関数を得ることができました。

なんというか、一般の人が乱数の研究をするために乱数ライブラリを使うことなんてまずないので、「指定した範囲の整数値を返すデフォルトの乱数生成器」とか定義されたものがひとつあると便利なのですが…。標準ライブラリにもそこまでは用意されていないようです。

どうもC++11の新ライブラリは理想を追求していて、ぱっと使うのが面倒なものが多いような気がします。

というわけで長い長い乱数の話でした。いずれはソフトウェア実装から離れて、マザーボードにハードウェア乱数生成器が標準搭載される時代が来たりするのでしょうか。


コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です