How it works...

The pseudo-random number library contains two types of components:

  • Engines, which are generators of random numbers; these could produce either pseudo-random numbers with a uniform distribution or, if available, actual random numbers.
  • Distributions that convert the output of an engine into a statistical distribution.

All engines (except for random_device) produce integer numbers in a uniform distribution, and all engines implement the following methods:

  • min(): This is a static method that returns the minimum value that can be produced by the generator.
  • max(): This is a static method that returns the maximum value that can be produced by the generator.
  • seed(): This initializes the algorithm with a start value (except for random_device, which cannot be seeded).
  • operator(): This generates a new number uniformly distributed between min() and max().
  • discard(): This generates and discards a given number of pseudo-random numbers.

The following engines are available:

  • linear_congruential_engine: This is a linear congruential generator that produces numbers using the following formula:

x(i) = (A * x(i-1) + C) mod M

  • mersenne_twister_engine: This is a Mersenne twister generator that keeps a value on W * (N-1) * R bits; each time a number needs to be generated, it extracts W bits. When all bits have been used, it twists the large value by shifting and mixing the bits so that it has a new set of bits to extract from.
  • subtract_with_carry_engine: This is a generator that implements a subtract with carry algorithm based on the following formula:
x(i) = (x(i - R) - x(i - S) - cy(i - 1)) mod M

  In the preceding formula, cy is defined as:

cy(i) = x(i - S) - x(i - R) - cy(i - 1) < 0 ? 1 : 0

In addition, the library provides engine adapters that are also engines wrapping another engine and producing numbers based on the output of the base engine. Engine adapters implement the same methods mentioned earlier for the base engines. The following engine adapters are available:

  • discard_block_engine: A generator that from every block of P numbers generated by the base engine keeps only R numbers, discarding the rest.
  • independent_bits_engine: A generator that produces numbers with a different number of bits than the base engine.
  • shuffle_order_engine: A generator that keeps a shuffled table of K numbers produced by the base engine and returns numbers from this table, replacing them with numbers generated by the base engine.

All these engines and engine adaptors are producing pseudo-random numbers. The library, however, provides another engine called random_device that is supposed to produce non-deterministic numbers, but this is not an actual constraint as physical sources of random entropy might not be available. Therefore, implementations of random_device could actually be based on a pseudo-random engine. The random_device class cannot be seeded like the other engines and has an additional method called entropy() that returns the random device entropy, which is 0 for a deterministic generator and nonzero for a non-deterministic generator. However, this is not a reliable method for determining whether the device is actually deterministic or non-deterministic. For instance, both GNU libstdc++ and LLVM libc++ implement a non-deterministic device, but return 0 for entropy. On the other hand, VC++ and boost.random return 32 and 10, respectively, for entropy.

All these generators produce integers in a uniform distribution. This is, however, only one of the many possible statistical distributions that random numbers are needed in most applications. To be able to produce numbers (either integer or real) in other distributions, the library provides several classes that are called distributions and are converting the output of an engine according to the statistical distribution it implements. The following distributions are available:

Type Class name Numbers Statistical distribution
Uniform uniform_int_distribution integer Uniform
uniform_real_distribution real Uniform
Bernoulli bernoulli_distribution boolean Bernoulli
binomial_distribution integer binomial
negative_binomial_distribution integer negative binomial
geometric_distribution integer geometric
Poisson poisson_distribution integer poisson
exponential_distribution real exponential
gamma_distribution real gamma
weibull_distribution real Weibull
extreme_value_distribution real extreme value
Normal normal_distribution real standard normal (Gaussian)
lognormal_distribution real lognormal
chi_squared_distribution real chi-squared
cauchy_distribution real Cauchy
fisher_f_distribution real Fisher's F-distribution
student_t_distribution real Student's t-distribution
Sampling discrete_distribution integer discrete
piecewise_constant_distribution real values distributed on constant subintervals
piecewise_linear_distribution real values distributed on defined subintervals

Each of the engines provided by the library has advantages and disadvantages. The linear congruential engine has a small internal state, but it is not very fast. On the other hand, the subtract with carry engine is very fast, but requires more memory for its internal state. The Mersenne twister is the slowest of them and the one that has the largest internal state, but when initialized appropriately can produce the longest non-repeating sequence of numbers. In the following examples, we will use std::mt19937, a 32-bit Mersenne twister with 19,937 bits of internal state.

The simplest way to generate random numbers looks like this:

    auto mtgen = std::mt19937 {}; 
for (auto i = 0; i < 10; ++i)
std::cout << mtgen() << std::endl;

In this example, mtgen is an std::mt19937 Mersenne twister. To generate numbers, you only need to use the call operator that advances the internal state and returns the next pseudo-random number. However, this code is flawed, as the engine is not seeded. As a result, it always produces the same sequence of numbers, which is probably not what you want in most cases.

There are different approaches for initializing the engine. One approach, common with the C rand library, is to use the current time. In modern C++, it should look like this:

    auto seed = std::chrono::high_resolution_clock::now() 
.time_since_epoch()
.count();
auto mtgen = std::mt19937{ static_cast<unsigned int>(seed) };

In this example, seed is a number representing the number of ticks since the clock's epoch until the present moment. This number is then used to seed the engine. The problem with this approach is that the value of that seed is actually deterministic, and in some classes of applications it could be prone to attacks. A more reliable approach is to seed the generator with actual random numbers. The std::random_device class is an engine that is supposed to return true random numbers, though implementations could actually be based on a pseudo-random generator:

    std::random_device rd; 
auto mtgen = std::mt19937 {rd()};

Numbers produced by all engines follow a uniform distribution. To convert the result to another statistical distribution, we have to use a distribution class. To show how generated numbers are distributed according to the selected distribution, we will use the following function. This function generates a specified number of pseudo-random numbers and counts their repetition in a map. The values from the map are then used to produce a bar-like diagram showing how often each number occurred:

    void generate_and_print( 
std::function<int(void)> gen,
int const iterations = 10000)
{
// map to store the numbers and their repetition
auto data = std::map<int, int>{};

// generate random numbers
for (auto n = 0; n < iterations; ++n)
++data[gen()];

// find the element with the most repetitions
auto max = std::max_element(
std::begin(data), std::end(data),
[](auto kvp1, auto kvp2) {
return kvp1.second < kvp2.second; });

// print the bars
for (auto i = max->second / 200; i > 0; --i)
{
for (auto kvp : data)
{
std::cout
<< std::fixed << std::setprecision(1) << std::setw(3)
<< (kvp.second / 200 >= i ? (char)219 : ' '),
}

std::cout << std::endl;
}

// print the numbers
for (auto kvp : data)
{
std::cout
<< std::fixed << std::setprecision(1) << std::setw(3)
<< kvp.first;
}

std::cout << std::endl;
}

The following code generates random numbers using the std::mt19937 engine with a uniform distribution in the range [1, 6]; that is basically what you get when you throw a dice:

    std::random_device rd{}; 
auto mtgen = std::mt19937{ rd() };
auto ud = std::uniform_int_distribution<>{ 1, 6 };
generate_and_print([&mtgen, &ud]() {return ud(mtgen); });

The output of the program looks like this:

In the next and final example, we change the distribution to a normal distribution with the mean 5 and the standard deviation 2. This distribution produces real numbers; therefore, in order to use the previous generate_and_print() function, the numbers must be rounded to integers:

    std::random_device rd{}; 
auto mtgen = std::mt19937{ rd() };
auto nd = std::normal_distribution<>{ 5, 2 };

generate_and_print(
[&mtgen, &nd]() {
return static_cast<int>(std::round(nd(mtgen))); });

The following will be the output of the earlier code:

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset