ミミです。いつもお世話になります。
クラスインスタンス毎に、ランダムな値(下記例では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;
}
>Q.
>クラスのコンストラクタで乱数ジェネレータの初期化を行い、そのクラスを2つ実体化す
ると、
>(Sleep()による待ちがない場合) rand()で得られる値は同一です。
>その理由は上記理由によるものなのでしょうか?
time()の精度が1秒のようですから、両方のコンストラクタが1秒以内に実行されれば、
生成される擬似乱数も同じ物になる。
かと思われますが……
マルチメディアタイマーの精度を1msにしてtimeGetTime()で取得するとか、
QueryPerformanceCounter()で高分解能パフォーマンスカウンタから下位unsigned int分
を使うとか。
そもそも乱数そのものの実体が一つなので、srandを複数回呼ぶのを避けるとか
の方が良い様な気もします。
これはsrand済みフラグとかを用意してそれを参照してやるとかで可能です。
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)の位置に書くのがベストでしょう。
これは、どちらかというと考え方(設計)の問題のような気がします。
なんのための srand() か?
まず、srand() が無かった場合を考えます。
この場合は、アプリケーションを再起動するたびに、全く同じ挙動を繰り返しま
す。これでは予測ができてしまいますので、それが問題となることが多いです。
そのため、srand()でアプリケ-ション開始時の挙動を変えるわけです。
では、srand()を呼びまくった場合を考えます。
まあ、それでも、rand()を呼ぶたびに違う値が戻ってくればそれでいいような気
もします。しかし、よく考えてみてください。
rand()はそれ自体、呼びだすたびに違う値を返す関数です。
srand()を呼びまくったのでは、rand()で乱数を発生させているのではなく、
srand()で乱数を発生させているような気になりませんか?
これって、結構な違和感じゃないですか?
これは私の持論ですが、違和感を覚えるコーディングは、様々なトラブルの原因
になります。
ですので、上記、
> そのため、srand()でアプリケ-ション開始時の挙動を変えるわけです。
の話通り、アプリケ-ション開始時のみ、srand()を呼ぶのが自然でよいと思います。
ミミです。
申し訳ありません。
まず、下記訂正します。
(誤)
>一方、ローカルで実体化しているから(クラスが静的領域に確保されているから)問題かと
(正)
>一方、ローカルで実体化しているから(クラスがスタック領域に確保されているから)問題かと
^^^^^^^^^^^^
# 静的変数は、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は同じになる
という事で解釈してもよろしいでしょうか。
(コーディングの仕方云々というのは抜きにして)
あっ・・麩さんが答えを導いてくださっていました。
>そもそも乱数そのものの実体が一つなので、
麩さんのご教授では、乱数そのものの実体は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も異なる。
という事と解釈しました。
皆様、ありがとうございました。
何度もすみません。
(誤)
↑(1)~(4)に於いて、Sleep()等がない限り、瞬時に実行される。
乱数ジェネレータを初期化する際に使用する値が同じ値のため、
(time()では1秒単位、GetTickCount()では1ミリ秒単位)
(2)と(4)で使用する値が変わらない限り、(3)では同じ単位により再初期化される。
(正)
↑(1)~(4)に於いて、Sleep()等がない限り、瞬時に実行される。
乱数ジェネレータを初期化する際に使用する値が同じ値のため、
(time()では1秒単位、GetTickCount()では1ミリ秒単位)
(1)と(3)で使用する値が変わらない限り、同じ値によって初期化されることにより、
(4)では(2)と同じ乱数が初期化される。