jeudi 22 mars 2018

Generically encapsulating random generators and distrubitons into a class template

I previously had a set of classes in one of my older libraries for working with the random number generators and distributions that was written when Visual Studio 2008 - 2010 were common place and before the release of 2012. I was starting a new project in Visual Studio 2017 so I decided to port my original version of the class over. It was giving me a plethora of errors. So I had to modify the original class, and when I tried to make a generic function to use the above classes, I ended up facing some downfalls due to the fact that you can not partial specialize function templates. After enough frustrations for the past few days; I started over and rewrote the whole class. With the some of the newer features of C++11 & higher; I was able to use variadic templates which truly simplified things once I got use to the required syntax. So now that you have a bit of the history I can move on to my working example.


I have this class Generator that is working without errors to the best of my knowledge; it compiles, builds and runs without errors and I have tested a handful of different combinations of engines or generators, different seeding techniques with different distributions using their template type T.

#ifndef GENERATOR_H
#define GENERATOR_H

#include <limits>
#include <chrono>
#include <random>
#include <type_traits>

enum SeedType { USE_CHRONO_CLOCK, USE_RANDOM_DEVICE, USE_SEED_VALUE, USE_SEED_SEQ };

template<class Engine, class Type, template<typename> class Distribution>
class Generator {
public:
    using Clock = std::conditional_t<std::chrono::high_resolution_clock::is_steady,
        std::chrono::high_resolution_clock,
        std::chrono::steady_clock>;

private:
    Engine _engine;
    Distribution<Type> _distribution;
    Type _value;

public:    
    template<class... Params>
    explicit Generator( Engine engine, Params... params ) : _engine( engine ) {
        _distribution = Distribution<Type>( params... );
    }

    void seed( SeedType type = USE_RANDOM_DEVICE, std::size_t seedValue = 0, std::initializer_list<std::size_t> list = {} ) {
        switch( type ) {
            case USE_CHRONO_CLOCK:  { _engine.seed( getTimeNow() );  break; }
            case USE_RANDOM_DEVICE: { std::random_device device{};
                                      _engine.seed( device() );      break; }
            case USE_SEED_VALUE:    { _engine.seed( seedValue );     break; }
            case USE_SEED_SEQ:      { std::seed_seq seq( list );
                                      _engine.seed( seq );           break; }
        }
    }

    void generate() { _value = _distribution( _engine ); }

    Type getGeneratedValue() const { return _value; }

    Distribution<Type> getDistribution() const { return _distribution; }

    std::size_t getTimeNow() {
        std::size_t now = static_cast<std::size_t>(Clock::now().time_since_epoch().count());
        return now;
    }    
};

#endif // !GENERATOR_H


Using it is as simple as this demonstrating a few examples:

#include <iostream>
#include <iomanip>
#include <vector>
#include "generator.h"

int main() {            
    // Engine, Seeding Type, & Distribution Combo 1
    std::mt19937 engine1;
    Generator<std::mt19937, short, std::uniform_int_distribution> g1( engine1, 1, 100 );
    g1.seed( USE_RANDOM_DEVICE );

    std::vector<short> vals1;
    for( unsigned int i = 0; i < 200; i++ ) {
        g1.generate();
        auto v = g1.getGeneratedValue();
        vals1.push_back( v );
    }

    int i = 0;
    for( auto& v : vals1 ) {

        if( (i % 10) != 0 ) {
            std::cout << std::setw( 3 ) << v << " ";
        } else {
            std::cout << '\n' << std::setw( 3 ) << v << " ";
        }       
        i++;
    }
    std::cout << "\n\n";

    // Engine, Seeding Type, & Distribution Combo 2
    std::ranlux48 engine2;
    std::initializer_list<std::size_t> list2{ 3, 7, 13, 17, 27, 31, 43 };   
    Generator<std::ranlux48, unsigned, std::binomial_distribution> g2( engine2, 50, 0.75 );
    g2.seed( USE_SEED_SEQ, std::size_t(7), list2 );

    std::vector<unsigned> vals2;
    for( int i = 0; i < 200; i++ ) {
        g2.generate();
        auto v = g2.getGeneratedValue();
        vals2.push_back( v );
    }

    i = 0;
    for( auto& v : vals2 ) {    
        if( (i % 10) != 0 ) {
            std::cout << std::setw( 3 ) << v << " ";
        } else {
            std::cout << '\n' << std::setw( 3 ) << v << " ";
        }
        i++;
    }
    std::cout << "\n\n";

    // Engine, Seeding Type, & Distribution Combo 3
    std::minstd_rand engine3;
    Generator<std::minstd_rand, float, std::gamma_distribution> g3( engine3, 0.22222f, 0.7959753f );
    g3.seed( USE_CHRONO_CLOCK );

    std::vector<float> vals3;    
    for( int i = 0; i < 200; i++ ) {
        g3.generate();
        auto v = g3.getGeneratedValue();
        vals3.push_back( v );
    }

    i = 0;
    for( auto& v : vals3 ) {

        if( (i % 5 ) != 0 ) {
            std::cout << std::setw( 12 ) << v << " ";
        } else {
            std::cout << '\n' << std::setw( 12 ) << v << " ";
        }
        i++;
    }
    std::cout << "\n\n";    

    std::cout << "\nPress any key and enter to quit.\n";
    std::cin.get();

    return 0;
}


Is this an appropriate way to generically encapsulate the random generators & distributions? Are there any gotchas that I'm missing? Finally, can this be improved for efficiency purposes? Or would it be more appropriate to ask this question on Code Review? If it is, please don't down vote and let me know and I will port it over there, or any other stack site that it would belong.




Aucun commentaire:

Enregistrer un commentaire