Search code examples
c++randomconstructorstdmersenne-twister

Incorrectly seeding Mersenne Twister via constructor


What is wrong with my constructor? Every time I call a function (about once every five seconds) that is supposed to generate random numbers, it generates the same numbers. Each call instantiates one of these objects below. I thought I was seeding m_gen randomly with the output of m_rd's operator() call.

Could I pass in the result of m_rd() to the constructor? What would the signature be? Shuffler(std::random device& rd)? But then that would be more difficult for the user.

Edit:

Actually, if it's possible, I would prefer a solution where you don't need to pass anything into the constructor.

shuffler.h

#include <random>

class Shuffler
{
private:
    std::random_device m_rd;
    std::mt19937 m_gen;
public:

    //! The default constructor. 
    Shuffler();

};

shuffler.cpp

#include "shuffler.h"

Shuffler::Shuffler() : m_gen(m_rd())
{
}

Solution

  • std::random_device is usually fine for this sort of thing, but it may not be on every platform. While most platforms' standard libraries implement it in terms of some underlying OS random functionality (i.e. /dev/urandom on Linux or CryptGenRandom on Windows), it is not required to do so by the C++ standard. On some platforms, high-quality random generators simply may not be available, and the standard allows std::random_device to be a simple, statically seeded PRNG. If it is, every std::random_device object will generate the same sequence of numbers.

    For those reasons, you may want to go back to simple time-seeding. The standard provides std::chrono::high_resolution_clock:

    class Shuffler
    {
    private:
        std::mt19937 m_gen;
    public:
        Shuffler()
            : m_gen{static_cast<std::uint32_t>(
                  std::chrono::high_resolution_clock::now().time_since_epoch().count()
              )}
        {}
    };
    

    std::chrono::high_resolution_clock usually has a resolution of nanoseconds or hundreds of nanoseconds. This is high enough that two PRNGs seeded by calls to the high_resolution_clock are very unlikely to end up using the same seed. This is also not guaranteed though. For example, std::chrono::high_resolution_clock only has microsecond resolution on macOS, which may or may not be good enough for your purposes.

    In the end, neither method is perfect. You may want to combine the two using std::seed_seq:

    std::seed_seq make_seeds() {
        thread_local std::random_device rd;
        return {{
            static_cast<std::uint32_t>(std::chrono::high_resolution_clock::now().time_since_epoch().count()),
            rd()
        }};
    }
    
    // Cast away rvalue-ness because the standard random generators need
    // an lvalue reference to their seed_seq for some strange reason
    template <typename T>
    T& identity(T&& t) { return t; }
    
    class Shuffler
    {
    private:
        std::mt19937 m_gen;
    public:
        Shuffler()
            : m_gen{identity(make_seeds())}
        {}
    };
    

    As you can see, this is getting far from simple, and it's still not perfect. See these blog posts for more information about seeding and random number generators then you ever thought you wanted.