7
\$\begingroup\$

I'm new to programming. I have a question regarding the usage of c++11 random header to generate random numbers. I tried to learn it, but was unsuccessful. Recently, I've tried the following approach and it worked. Any advice on how to improve it. Is there a good source to learn more about this?

Here's the code:

#include <iostream>
#include <random>
#include <ctime>
int main()
{
   std::default_random_engine random_engine(time(0));
   for (int i = 1; i <= 100; i++)
   {
    std::uniform_int_distribution<int> num(1, 100);
    std::cout << i << "==> " << num(random_engine) << std::endl;
   }
    return 0;
}
\$\endgroup\$
8
\$\begingroup\$

First of all, note that std::default_random_engine and std::uniform_int_distribution<int> are both non-portable in the same sense as rand() is non-portable: they won't produce the same sequence of numbers on different platforms, because their behavior is implementation-defined. For uniform_int_distribution there effectively is no portable alternative, so it's the best game in town. But for the PRNG itself, I don't think any expert would ever recommend default_random_engine. Just pick a known engine, such as std::mt19937. (Which isn't good but again it's the only game in town at the moment.)

For seeding: notice that you're giving only 32 bits of seed (and a very predictable seed at that) to the PRNG's constructor. You should be seeding from a random_device, something like this (stolen from here):

std::random_device rd;
int data[624];
std::generate_n(data, std::size(data), std::ref(rd));
std::seed_seq sseq(data, std::end(data));
std::mt19937 g(sseq);

(Of course nobody does this in practice.)


for (int i = 1; i <= 100; i++)

Prefer

for (int i = 0; i < 100; ++i)

even if it means you have to refer to i+1 inside the loop. Using half-open ranges that start at 0 is good practice for everywhere in C and C++ and every modern language.


std::uniform_int_distribution<int> num(1, 100);

Personally I'd write this as

auto dist = std::uniform_int_distribution<int>(1, 100);

to get that nice clear = separation visible at a glance. Also, notice my conventional variable names throughout: rd for the random device, g for the PRNG itself, dist for the distribution. It might make sense to use a more descriptive name than dist... but certainly num is not more descriptive, and num is less truthful — a distribution is not a single number!


Finally, you don't need that final return 0 (main returns 0 on success by default), and you don't need std::endl because '\n' (or "\n") will do just as well.

But basically you're doing the dance correctly: PRNG created outside the loop, distribution called-like-a-function from inside the loop.

You could move the variable definition of the distribution outside the loop as well, if you wanted. It probably doesn't matter in practice because uniform_int_distribution is unlikely to have any state worth preserving between iterations. But if it were a std::normal_distribution, say, then destroying and recreating the distribution every time through the loop would be costing you basically 50% of your speed.

auto dist = std::uniform_int_distribution<int>(1, 100);
for (int i = 0; i < 100; ++i) {
    std::cout << (i+1) << "==> " << dist(g) << '\n';
}
\$\endgroup\$
4
  • 1
    \$\begingroup\$ Isn't std::default_random_engine alias to std::mt19937? \$\endgroup\$ – ALX23z Dec 25 '20 at 11:26
  • 1
    \$\begingroup\$ @ALX23z: In MSVC's STL, yes. In libstdc++, no, it's an alias to std::minstd_rand0. In libc++, no, it's an alias to std::minstd_rand which is different from minstd_rand0. It's different on every platform, which is why I call it "non-portable" in the same sense as the old libc rand(). godbolt.org/z/1a33nW \$\endgroup\$ – Quuxplusone Dec 25 '20 at 18:20
  • \$\begingroup\$ This is a great example of the problems with the C++-11 random number generator: many applications don't need high-quality randomness, they need easy-to-use randomness. Putting a call to srand(time(NULL)) at program startup is a lot easier than faffing about with the contents of <random>. \$\endgroup\$ – Mark Dec 26 '20 at 2:35
  • \$\begingroup\$ @Mark: FWIW I think we agree on that point. OP's auto g = std::mt19937(time(nullptr)); (well, that's what OP should have written ;)) will be okay-ish, and std::random_device rd; auto g = std::mt19937(rd()); would be state-of-the-industry. When I said "you should be using seed_seq..." followed by "...of course nobody does this in practice," I meant both parts of what I said. ;) seed_seq is just abysmal API design. \$\endgroup\$ – Quuxplusone Dec 26 '20 at 2:50
5
\$\begingroup\$

I’m not aware of any sources that give a really good intro to the standard <random> library, unfortunately. I wrote about it years ago, but that was when it was brand new, and I was mostly just introducing it; it’s not really a comprehensive tutorial.

However, I can give some advice about the code you’ve got.

std::default_random_engine random_engine(time(0));

It’s generally unwise to use std::time() to seed a random number generator, for a number of reasons.

First, there’s no guarantee that the return type can actually be used as a seed, because it might not be convertible to an unsigned integer type. (In practice, I’ve never seen a platform where this is the case, because the return type is a 32-bit signed int on all POSIX platforms).

Second, the return type is usually (probably almost always) only good to about 1 second. That means that if the program runs twice within a second, it will generate exactly the same “random” sequence.

Third, it is a predictable value. If I want to control your program, all I have to do is set my clock to a time that generates the sequence I want.

In practice, the usual way to seed a random engine is with std::random_device. It is usually non-deterministic these days (so, completely unpredictable), but it’s one of those things that isn’t guaranteed to work properly everywhere (though it will almost certainly work properly everywhere most people will ever need it).

So what you’d do would be this:

std::random_device rd;
std::default_random_engine random_engine(rd());

// or, in one line:
std::default_random_engine random_engine(std::random_device{}());

Next:

for (int i = 1; i <= 100; i++)
{
    std::uniform_int_distribution<int> num(1, 100);
    std::cout << i << "==> " << num(random_engine) << std::endl;
}

It might be more efficient to move the distribution out of the loop:

std::uniform_int_distribution<int> dist(1, 100);

for (int i = 1; i <= 100; i++)
{
    std::cout << i << "==> " << dist(random_engine) << '\n';
}

Finally, a few quick tips:

  1. The only engines I have found cause to use are mersenne_twister_engine and linear_congruential_engine. mt19937 (which is a mersenne_twister_engine) should be your default choice. I’ve used LCGs in cases like simulations where each entity has its own random number generator, because Mersenne twisters are great… but also huge and relative slow. LCGs are tiny and fast, but don’t produce great randomness… but sometimes that’s okay.
  2. The only distributions I have found cause to use regularly are uniform_int_distribution and normal_distribution.
    • Uniform distributions are for cases where you want a number between two values, and every value between is equally likely—for example, rolling a die or picking a value out of a group (like pulling a name out of a hat).
    • Normal distributions are for cases where you want “natural-looking” randomness—for example, the damage done after a hit in a game (the “nominal” damage done by an attack might be 100, then you use normally-distributed randomness to be “thereabouts”, so 99 or 101 is more likely than 95 or 105). I have used other distributions, like the bernoulli_distribution, to simulate something that has, say, a 62% chance of succeeding being done over and over. But even for that, I usually just use a normal distribution.
  3. If you need to change the range of a distribution, you can either just assign it with a new value, or use the param() function.
\$\endgroup\$
3
  • \$\begingroup\$ "I’m not aware of any sources that give a really good intro" There are some IMO very good videos from cppcon that gives an intro to the library: youtu.be/0Ez-KqDTVXg and youtu.be/6DPkyvkMkk8 \$\endgroup\$ – Winther Dec 25 '20 at 20:16
  • \$\begingroup\$ In regards to seeding with the current time, most programs aren't started more than once per second, and don't use randomness in security-critical ways. For example, by picking a startup time, you can control the order of random jumps through my photo album -- so what? \$\endgroup\$ – Mark Dec 26 '20 at 2:38
  • \$\begingroup\$ It doesn’t need to be “security-critical” to be a bug. For example, a game where you can fiddle with the PRNG will be easy to “cheat”, and you may say “who cares? they’re just spoiling their own fun”… well, maybe, except when it comes to multiplayer play, or speed-running, or the like; you never know. Really, the bottom line is that if you think you can get away with doing things poorly… 🤷🏼 hey, man, it’s your code, do as you please. Just don’t be surprised when I prefer to buy the app from the coder who does things according to best practices. \$\endgroup\$ – indi Dec 27 '20 at 4:01

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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