11
\$\begingroup\$

I wrote a small function to return a random in a given range:

int random_in_range(int min, int max) {
    std::random_device rd;
    std::mt19937 rng(rd());
    std::uniform_int_distribution<int> uni(min, max);
    return uni(rng);
}

But I read somewhere that you should only seed a random number generator once leading me to believe that the function should really be:

std::random_device rd;
std::mt19937 rng(rd());
int random_in_range(int min, int max) {
    std::uniform_int_distribution<int> uni(min, max);
    return uni(rng);
}

I later tested both to see if one was clearly better than the other (in terms of randomness) and got results which do not make things any clearer.

First example result with 10 runs, making a decision of 1 or 0:

for (int i = 0; i < 10; i++) {
    cout << first_example(0, 1);
}
>0100100001

The second example result with 10 runs, making a decision of 1 or 0:

 for (int i = 0; i < 10; i++) {
    cout << second_example(0, 1);
}
>1011000110

The two results don't seem too strange leading me to be confused about how I should initialize random number generators. Basically, what I am asking is: which of these two example (or something else if both are wrong) would be used in order to guarantee the lowest amount of bias?

\$\endgroup\$
11
  • 1
    \$\begingroup\$ If you want to seed your random number generator only once, then move the declaration of rng out of your function, too. Moving the random device outside is not sufficient. \$\endgroup\$ Commented Aug 21, 2015 at 0:11
  • 4
    \$\begingroup\$ It's unclear what exactly you expect from your output. 10 samples of a random number generator shows nothing about the actual performance of the RNG. \$\endgroup\$ Commented Aug 21, 2015 at 0:14
  • 4
    \$\begingroup\$ Yes, you should seed your random number generator once. What I was pointing out was that neither of your examples actually do that. \$\endgroup\$ Commented Aug 21, 2015 at 0:23
  • 2
    \$\begingroup\$ @CoffeeConverter - if the result of one of those runs had been 0000000000 would it make you nervous? That's a trick question: any truly random sequence of ten zeros and ones is just as likely as any other sequence, and there's nothing suspicious about seeing a sequence of ten zeros. Ten values just isn't enough to draw any conclusions about the quality of a random number generator. \$\endgroup\$ Commented Aug 21, 2015 at 13:46
  • 2
    \$\begingroup\$ @CoffeeConverter: You should read about on Gamblers Fallacy Streaks of one result are quite common in long series of data. The chance of getting 10 heads in a row (somewhere in the sequence) over a thousand run sequence is actually quite high 62% \$\endgroup\$ Commented Aug 21, 2015 at 17:33

3 Answers 3

12
\$\begingroup\$

If you were going to get a number from random_device at every call, you might as well just use it directly:

int random_in_range(int min, int max) {
    std::random_device rd;
    std::uniform_int_distribution<int> uni(min, max);
    return uni(rd());
}

std::random_device is intended to be a front-end for a truly random bit source. The major shortcoming is that in many cases it has fairly limited bandwidth, so you'd prefer to avoid calling it every time you need a number.

If you do want to use mt19937 (a perfectly fine idea in many cases) I'd personally use a function-object instead of a function:

class random_in_range { 
    std::mt19937 rng;
public:
    random_in_range() : rng(std::random_device()()) {}
    int operator()(int low, int high) { 
        std::uniform_int_distribution<int> uni(low, high);
        return uni(rng);
    }
};

This does have some shortcoming though: people may use a temporary of this type in a loop:

for (int i=0; i<10; i++)
    std::cout << random_in_range()(0, 1);

...which puts you back where you started. You need to do something like:

random_in_range r;
for (int i=0; i<10; i++)
    std::cout << r(0, 1);

...to get the results you want (i.e., seed once, call multiple times).

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Another significant point about std::random_device is that it is generally non-deterministic rather than a PRNG. Your point about using it sparingly is important. \$\endgroup\$
    – Edward
    Commented Aug 21, 2015 at 1:22
  • \$\begingroup\$ I know is does not matter for uniform_int_distribution, but I seem to remember that distributions should also be kept from one iteration to the other (see for example the poisson_distribution example). \$\endgroup\$ Commented Aug 21, 2015 at 13:05
11
\$\begingroup\$

Here's how Bjarne Stroustrup did it:

// random number generator from Stroustrup: 
// http://www.stroustrup.com/C++11FAQ.html#std-random
int rand_int(int low, int high)
{
    static std::default_random_engine re {};
    using Dist = std::uniform_int_distribution<int>;
    static Dist uid {};
    return uid(re, Dist::param_type{low,high});
}

The principal difference is that the random engine re is static so there is only one initialization (and therefore seed).

Also note that a sample of 10 runs is too short to conclude much. Testing random number generators (RNGs) or psuedo-random number generators (PRNGs) is quite complex. See http://csrc.nist.gov/groups/ST/toolkit/rng/index.html for a thorough explanation of the purpose, theory and actual source-code for tools to do a good job of testing PRNGs.

\$\endgroup\$
4
  • 2
    \$\begingroup\$ This function is not thread safe. \$\endgroup\$
    – Siyuan Ren
    Commented Aug 21, 2015 at 6:09
  • 1
    \$\begingroup\$ @SiyuanRen: Indeed, for thread safety, the static bits should be thread local. \$\endgroup\$ Commented Aug 21, 2015 at 13:07
  • 1
    \$\begingroup\$ The context of Stroustrup's code was to show a simple random number generator that could be used by beginning students, not to show the universally "best" way to do it. Beginners are unlikely to write multithreaded code. \$\endgroup\$
    – Edward
    Commented Aug 21, 2015 at 13:33
  • 2
    \$\begingroup\$ @Edward - beginners are far too likely to write (bad) multithreaded code. Nevertheless, you're absolutely right that simple examples should be simple. \$\endgroup\$ Commented Aug 21, 2015 at 13:42
7
\$\begingroup\$

You can simply make your re-usable objects thread_local statics:

int random_in_range(int min, int max) {

    thread_local static std::mt19937 mt(std::random_device{}());
    thread_local static std::uniform_int_distribution<int> pick;

    // assuming param_type is lighter weight to construct
    // than a uniform_int_distribution
    return pick(mt, decltype(pick)::param_type{min, max});
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.