コンストラクタでの乱数設定について

[戻る]
ミミ 2008/04/22(火) 22:02:56
ミミです。いつもお世話になります。

クラスインスタンス毎に、ランダムな値(下記例では1〜10)を m_nVal に設定したいと
考えています。
インスタンスは2つ作ります。

その際、
(1)の位置で乱数ジェネレータの初期化を行うと、自分の意図する通り(hoge1、hoge2共に
別の値)
に動きますが、(2)の位置で乱数ジェネレータの初期化を行うと、2つのインスタンスの 
m_nVal 
は常に同じ値を返します。(偶々?)

http://rararahp.cool.ne.jp/cgi-bin/lng/vc/vclng.cgi?print+200512/05120007.txt
のスレッドより、
(2)の位置で初期化を行う場合、hoge1とhoge2は、ほぼ同じタイミングで実体化されるこ
とから
初期化されたテーブルも同じ物になるのかと予想しています。

そこで、hoge1の実体化の後、Sleep() (要Windows.hのインクルード)を入れて、時間をお
いて
hoge2を実体化したところ、確かに m_nVal の値は変わりました。

Q.
クラスのコンストラクタで乱数ジェネレータの初期化を行い、そのクラスを2つ実体化す
ると、
(Sleep()による待ちがない場合) rand()で得られる値は同一です。
その理由は上記理由によるものなのでしょうか?



一方、ローカルで実体化しているから(クラスが静的領域に確保されているから)問題かと
考え、
下記の様に動的領域に確保してみました。

CHoge*    pHoge1;
CHoge*    pHoge2;

pHoge1 = new CHoge();
pHoge2 = new CHoge();

cout << pHoge1->m_nVal << endl;
cout << pHoge2->m_nVal << endl;

delete pHoge1;
delete pHoge2;

と試してみましたが、この場合も同様に2つの m_nVal は同じ値でした。


識者の皆様、なにとぞ、よろしくお願いいたします。
(VC6、Win2000、コンソールアプリで試してみました)

---------------------------------------------
#include <stdlib.h>
#include <iostream.h>
#include <time.h>


class CHoge{
public:
    CHoge();
    int    m_nVal;
};

void main()
{
//    srand( (unsigned)time( NULL ) ); // <----(1)

    CHoge    hoge1;
    CHoge    hoge2;

    cout << hoge1.m_nVal << endl;
    cout << hoge2.m_nVal << endl;
}

CHoge::CHoge()
{
//    srand( (unsigned)time( NULL ) ); // <----(2)
    m_nVal = (rand() % 10) + 1;
}

瀬戸っぷ [E-Mail] 2008/04/23(水) 00:16:53
>Q.
>クラスのコンストラクタで乱数ジェネレータの初期化を行い、そのクラスを2つ実体化す
ると、
>(Sleep()による待ちがない場合) rand()で得られる値は同一です。
>その理由は上記理由によるものなのでしょうか?

time()の精度が1秒のようですから、両方のコンストラクタが1秒以内に実行されれば、
生成される擬似乱数も同じ物になる。
かと思われますが……

マルチメディアタイマーの精度を1msにしてtimeGetTime()で取得するとか、
QueryPerformanceCounter()で高分解能パフォーマンスカウンタから下位unsigned int分
を使うとか。

2008/04/23(水) 02:56:35
そもそも乱数そのものの実体が一つなので、srandを複数回呼ぶのを避けるとか
の方が良い様な気もします。
これはsrand済みフラグとかを用意してそれを参照してやるとかで可能です。

たいちう 2008/04/23(水) 09:23:29
CHoge::CHoge() {
    static bool first = true;
    if (first) {
        first = false;
        srand( (unsigned)time( NULL ) );
        m_nVal = (rand() % 10) + 1;
    }
}

このような処理で、srandを一度だけ呼び、その呼び出しをmain()から
隠蔽することはできますが、お勧めしません。
将来、似たようなクラスCPiyoが必要になったときに、
何故かCHogeとCPiyoが同じ値になってしまう、という現象に悩むかもしれませんので。

srand済みフラグを用意するならば、そのスコープはグローバルにする必要がありますので、
フラグを使うぐらいならば、(1)の位置に書くのがベストでしょう。

bun 2008/04/23(水) 11:49:12
これは、どちらかというと考え方(設計)の問題のような気がします。
なんのための srand() か?

まず、srand() が無かった場合を考えます。
この場合は、アプリケーションを再起動するたびに、全く同じ挙動を繰り返しま
す。これでは予測ができてしまいますので、それが問題となることが多いです。
そのため、srand()でアプリケ-ション開始時の挙動を変えるわけです。

では、srand()を呼びまくった場合を考えます。
まあ、それでも、rand()を呼ぶたびに違う値が戻ってくればそれでいいような気
もします。しかし、よく考えてみてください。
rand()はそれ自体、呼びだすたびに違う値を返す関数です。
srand()を呼びまくったのでは、rand()で乱数を発生させているのではなく、
srand()で乱数を発生させているような気になりませんか?
これって、結構な違和感じゃないですか?
これは私の持論ですが、違和感を覚えるコーディングは、様々なトラブルの原因
になります。

ですので、上記、
> そのため、srand()でアプリケ-ション開始時の挙動を変えるわけです。
の話通り、アプリケ-ション開始時のみ、srand()を呼ぶのが自然でよいと思います。

ミミ 2008/04/23(水) 12:19:55
ミミです。

申し訳ありません。
まず、下記訂正します。

(誤)
>一方、ローカルで実体化しているから(クラスが静的領域に確保されているから)問題かと

(正)
>一方、ローカルで実体化しているから(クラスがスタック領域に確保されているから)問題かと
                      ^^^^^^^^^^^^
# 静的変数は、staticキーワードで宣言した場合でした。

----------------------------------------------

瀬戸っぷさん、麩さん、たいちうさん、bunさん、ご教授ありがとうございます。

瀬戸っぷさんのおっしゃる通り、精度がミリセカンドの関数を使用して試してみました。
ジェネレータの初期化時に、timeGetTime() 及び GetTickCount()を使用して試したところ、
インスタンスを作るタイミングを

    CHoge    hoge1;
    Sleep(1);
    CHoge    hoge2;

の様にしただけで、それぞれの m_nValは別の値になりました。


麩さん、たいちうさんのご助言、ジェネレータの初期化そのものを1回のみにするという
手法では、確かに自分の意図する通りに動作します。

bunさんのご助言「何のためにsrand()か」ですが、main()全般を通して1回だけ
初期化を行うというのが、正直今まで自分が暗黙的に持っていた考え方でした。

しかし、クラスの実体化を考えた場合、今回はクラスのコンストラクタでrand()を実行
している為、srand()による初期化もクラス毎で行うべきなのかとも考えました。
どうも、クラス内での初期化と、main()内での初期化は別物(スコープの考え方)だと
考えてしまいます。

もちろん、srand()が乱数を発生させている関数とは解釈してはいませんし、
違和感を覚えるコーディングは、後々になっても苦労するだけの為、
開始時に1回だけの方が良いというご助言も理解できます。


すみません、まとめさせてください。
今回、コンストラクタで乱数ジェネレータの初期化を行い、hoge1とhoge2の実体化の
間に待ちが無い場合、m_nValは同じ値となりますが、それは

(1)初期化に使用していたtime精度が1秒単位
 &
(2)2つの実体化がほぼ同じタイミングのため、初期化に使用する値も(1)により同じ為、
   結果、m_nValは同じになる

という事で解釈してもよろしいでしょうか。
(コーディングの仕方云々というのは抜きにして)

ミミ 2008/04/23(水) 12:32:43
[[解決]]
あっ・・麩さんが答えを導いてくださっていました。


>そもそも乱数そのものの実体が一つなので、

麩さんのご教授では、乱数そのものの実体は1つですので、

順序的に
(1)hoge1のコンストラクタで乱数を初期化
(2)乱数取得
(3)hoge2のコンストラクタで乱数を初期化
(4)乱数取得

↑(1)〜(4)に於いて、Sleep()等がない限り、瞬時に実行される。
 乱数ジェネレータを初期化する際に使用する値が同じ値のため、
  (time()では1秒単位、GetTickCount()では1ミリ秒単位)
 (2)と(4)で使用する値が変わらない限り、(3)では同じ単位により再初期化される。

よって、(2)と(3)との間に、time()では1秒以上、GetTickCount()では1ミリ秒以上
の時間があれば、(2)と(4)で得られる m_nVal は異なるし、
main()開始時又は hoge1 のコンストラクタでのみ初期化を行えば、(2)と(4)の乱数
取得は((3)による再初期化を伴わない)連続的なものになる為、m_nValも異なる。

という事と解釈しました。
皆様、ありがとうございました。

ミミ 2008/04/23(水) 12:34:51
何度もすみません。

(誤)
↑(1)〜(4)に於いて、Sleep()等がない限り、瞬時に実行される。
 乱数ジェネレータを初期化する際に使用する値が同じ値のため、
  (time()では1秒単位、GetTickCount()では1ミリ秒単位)
 (2)と(4)で使用する値が変わらない限り、(3)では同じ単位により再初期化される。


(正)
↑(1)〜(4)に於いて、Sleep()等がない限り、瞬時に実行される。
 乱数ジェネレータを初期化する際に使用する値が同じ値のため、
  (time()では1秒単位、GetTickCount()では1ミリ秒単位)
 (1)と(3)で使用する値が変わらない限り、同じ値によって初期化されることにより、
 (4)では(2)と同じ乱数が初期化される。

古いスレッドには追加発言できません。