この記事はC++ Advent Calendar 2018 の12日目の記事です。
そして今日の日付は2018年12月16日です。
……ごめんなさい。期日に遅れました。
11日目→@kizulさん なぜC++のclassとstructは同じ機能なのか - Qiita
13日目→@tyanmahouさん C++標準属性まとめ - Qiita
C++の乱数ライブラリが使いにくい話 - Qiita
(というか今気づいたけど、以下で俺が書いているような難しさ面倒臭さを一気に解決してくれる便利なクラスを
今年の2日目の@Gacchoさんが書いていますね……
お手軽 乱数実装【C++11】 - Qiita)
環境
コンパイラ:Visual Studio 2017 Version 15.9.3
OS: Windows 7 64bit
ライブラリの単純な使用法
単純な使い方の例を以下に示す。
#include "pch.h" #include <iostream> // https://cpprefjp.github.io/reference/random.html のリンク先の各ライブラリからコピー、改変 #include <random> int main() { std::random_device seed_gen; std::default_random_engine engine(seed_gen()); // 0以上9以下の値を等確率で発生させる std::uniform_int_distribution<> dist1(0, 9); for (std::size_t n = 0; n < 50; ++n) { // 一様整数分布で乱数を生成する int result = dist1(engine); std::cout << result << ","; } std::cout << "\n\n"; // 平均0.0、標準偏差1.0で分布させる std::normal_distribution<> dist2(0.0, 1.0); for (std::size_t n = 0; n < 50; ++n) { // 正規分布で乱数を生成する double result = dist2(engine); std::cout << result << ","; } std::cout << "\n\n"; // 確率0.7でtrueを生成し、確率0.3(1.0 - 0.7)でfalseを生成する std::bernoulli_distribution dist3(0.7); for (std::size_t n = 0; n < 50; ++n) { // 乱数を生成する bool result = dist3(engine); std::cout << result << ","; } std::cout << "\n\n"; }
基本的に、
- random_deviceクラスのインスタンスを作る
- default_random_engineクラスのインスタンスを作って、その引数(=乱数シード)として1番から生成した乱数を入れる
- 自分の目的に合った乱数クラス(例えばuniform_int_distributionクラス)のインスタンスを作って、その引数に2番から生成した乱数を入れる
……面倒だね。
問題設定の例
……ところが、乱数を生成して使用する箇所は、実際にはmain関数の中ではなく各クラスの中が多いだろう。
単純な例を考えて作ってみた。
- 2人ですごろくをする
- マスは全部で30マス (ちょうどに止まらなくても通過すれば上がり)
- 2, 12, 22 のマスに止まったら「サイコロを振り、4以上の目が出たら3マス進む」
- 7, 17, 27 のマスに止まったら「サイコロを振り、4以上の目が出たら3マス戻る」
大したゲームでもないけど、コンソールの出力は例えば以下のようになる。
Aさんは6が出たので、6に進みました Bさんは4が出たので、4に進みました Aさんは4が出たので、10に進みました Bさんは4が出たので、8に進みました Aさんは6が出たので、16に進みました Bさんは3が出たので、11に進みました Aさんは5が出たので、21に進みました Bさんは1が出たので、12に進みました 6が出たので、Bさんは盤面の効果により、15に進みました Aさんは1が出たので、22に進みました 1が出たので、動きません Bさんは6が出たので、21に進みました Aさんは2が出たので、24に進みました Bさんは6が出たので、27に進みました 1が出たので、動きません Aさんは3が出たので、27に進みました 4が出たので、Aさんは盤面の効果により、24に戻りました Bさんは1が出たので、28に進みました Aさんは3が出たので、27に進みました 3が出たので、動きません Bさんは5が出たので、33に進みました Bさんの勝ちです
rand()を用いた実装
設計に自信がないけど、Player / Board / Dice クラスを作って以下のようにした。
/*player.cpp*/ int Player::turn() { int num = dice->cast(); position += num; std::cout << name << "さんは" << num << "が出たので、" << position <<"に進みました\n"; if (position >= board->goal_position) { std::cout << name << "さんの勝ちです\n"; has_ended = true; } if (board->HasEffect(position)) { board->ApplyEffect(this); } // 改めてゴール判定はしない:3マス進んでゴールに行くことは無いことを暗黙裏に仮定 // 改めて効果判定はしない:効果マスで進んだり戻ったりした先が効果マスでないことを暗黙裏に仮定 return 0; }
まず単純にrand()関数を使う方式だと、Diceクラスの中身は以下のようになる。
#include <stdlib.h> #include <time.h> void Dice::init() { srand((unsigned)time(NULL)); } int Dice::cast() { return rand() % 6 + 1; }
全貌はここには書かないので以下を参照。
この例に限らず一般の場合にも、サイコロ(または類似の乱数)のクラスを作って、呼ばれたらサイコロを振った値を返すようにしたいじゃん。多分。
しかし、rand関数を使ったやり方だと、以下の問題点がある。
- 厳密にはそれぞれの目が等確率ではない(rand関数は32768通りの値を返すがこれは6の倍数ではない)
- 乱数の品質が良くない
- 自分の目的に合った乱数に計算し直す必要がある(1~6の整数ならまだしも、正規分布やポアソン分布に従う乱数をrand()から計算するのは一苦労だ)
というわけで、randomライブラリを使って書くにはどうすれば良いのだろう。
一様初期化を用いた実装
/* header.h */ #pragma once #include <iostream> #include <string> #include <random> class Player; class Dice { public: int cast(); Dice() { ; } std::random_device seed_gen; std::default_random_engine engine{ seed_gen() }; std::uniform_int_distribution<> dist1{1,6}; }; /* dice.cpp */ #include "header.h" int Dice::cast() { return dist1(engine); }
std::uniform_int_distribution<> dist1{1,6}
という書き方は、
- 非静的メンバ変数を定義するときに初期化する https://cpprefjp.github.io/lang/cpp11/non_static_data_member_initializers.html
- 波カッコ { }による一様初期化 (uniform initialization。) https://cpprefjp.github.io/lang/cpp11/uniform_initialization.html
の合わせ技である。
dist1(1,6)と普通のカッコを使うとエラーになる。dist1という名前のメンバ関数を定義したと解釈されてしまうからだ。
上記の2項目はC++11の機能である。
コンパイラの実装状況 - cpprefjp C++日本語リファレンスによれば、S2013以降で対応となっている。
(俺はある場所ではVS2012を使っているのでこの方法が使えない。つらい。)
他の方法はあるんだろうか? と考えたのが、下のメンバイニシャライザの方法である。
メンバイニシャライザ
乱数ライブラリのインスタンスをクラスのメンバ変数にしたい。
このメンバ変数に引数を与えて初期化をしたい、というのが、今やりたいことである。
したがって、メンバイニシャライザ(メンバ初期化子) を使えば目的は達成できる。
/* header.h */ #pragma once #include <iostream> #include <string> #include <random> class Dice { public: int cast(); Dice(); std::random_device seed_gen; std::default_random_engine engine; std::uniform_int_distribution<> dist1; }; /* dice.cpp */ Dice::Dice(): engine(seed_gen()), dist1(1,6) { } int Dice::cast() { return dist1(engine); }
Dice::Dice()の中でメンバイニシャライザを使って初期化すれば良い。
(メンバイニシャライザを検索してもいまいち情報が出てこなくて自信がないけど、C++11や14の機能じゃないよね?)
エラーになる例
メンバイニシャライザを使わずにコンストラクタの中で初期化することはできない。
コンストラクタ内部に書くと、()演算子の呼び出しであって、初期化にはならないからだ。
Dice::Dice() { engine(seed_gen()); //エラー dist1(1, 6); //エラー }
E0304 オーバーロードされた関数 "std::uniform_int_distribution<_Ty>::operator() [代入_Ty=int]" のインスタンスが引数リストと一致しません E0304 関数 "std::mersenne_twister<_Ty, _Wx, _Nx, _Mx, _Rx, _Px, _Ux, _Sx, _Bx, _Tx, _Cx, _Lx>::operator() [代入_Ty=unsigned int, _Wx=32, _Nx=624, _Mx=397, _Rx=31, _Px=2567483615U, _Ux=11, _Sx=7, _Bx=2636928640U, _Tx=15, _Cx=4022730752U, _Lx=18]" のインスタンスが引数リストと一致しません
という、めっちゃ長いエラーメッセージに襲われることになる。
以上。それでは。