C++ randomライブラリをクラス内で使う方法

この記事はC++ Advent Calendar 2018 の12日目の記事です。
そして今日の日付は2018年12月16日です。
……ごめんなさい。期日に遅れました。

11日目→@kizulさん なぜC++のclassとstructは同じ機能なのか - Qiita
13日目→@tyanmahouさん C++標準属性まとめ - Qiita

ライブラリをクラスの中で使うにはどうすれば良いんだろう、という話。

ライブラリの単純な使い方は、2016年のアドベントカレンダーで @_EnumHackさんが書いている。
C++の乱数ライブラリが使いにくい話 - Qiita

(というか今気づいたけど、以下で俺が書いているような難しさ面倒臭さを一気に解決してくれる便利なクラスを 今年の2日目の@Gacchoさんが書いていますね……
お手軽 乱数実装【C++11】 - Qiita)

環境

コンパイラVisual Studio 2017 Version 15.9.3
OS: Windows 7 64bit

ライブラリの単純な使用法

ライブラリはC++11で追加された。色々な分布に従う乱数を生成できるのが特徴だ。しかしその代わりに、使い方が少々面倒だ。
単純な使い方の例を以下に示す。

#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";

}

基本的に、ライブラリを使うときは以下の3段階のステップを踏めば良い。

  1. random_deviceクラスのインスタンスを作る
  2. default_random_engineクラスのインスタンスを作って、その引数(=乱数シード)として1番から生成した乱数を入れる
  3. 自分の目的に合った乱数クラス(例えば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;
}

全貌はここには書かないので以下を参照。

github.com

この例に限らず一般の場合にも、サイコロ(または類似の乱数)のクラスを作って、呼ばれたらサイコロを振った値を返すようにしたいじゃん。多分。

しかし、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} という書き方は、

の合わせ技である。
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]" のインスタンスが引数リストと一致しません

という、めっちゃ長いエラーメッセージに襲われることになる。

以上。それでは。