В общем инкапсуляция генераторы случайных и распределения в шаблон класса


Раньше у меня был набор классов, в одном из моих старых библиотек для работы с генераторами случайных чисел и распределения, которая была написана, когда Visual Studio 2008 и 2010 гг. были банальными и до выпуска 2012 года. Я начинаю новый проект в Visual Studio в 2017, поэтому я решил портировать оригинальную версию класса. Он давал мне множество ошибок, так что мне пришлось изменить первоначальный класс. Когда я попытался сделать универсальную функцию для использования перечисленных выше классов, я оказался перед несколько падений из-за того, что вы не можете частично специализировать шаблоны функций. После достаточно разочарований за последние несколько дней, я начал снова и переписал весь класс с нуля.

С некоторыми из новых возможностей стандарта C++11 и выше, я был в состоянии использовать шаблоны с переменным числом аргументов, которая действительно упрощает как-то я привык необходимый синтаксис.

У меня есть этот класс Generator которая работает без ошибок, насколько мне известно; он собирает, строит и работает без ошибок и я испытал множество различных комбинаций двигателей и генераторов, различных сеялок с различными дистрибутивами.

#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

Использовать его так же просто, как это демонстрируют несколько примеров:

#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;
}

Это подходящий способ для общего инкапсуляции генераторы случайных и дистрибутивы? Нет ли подводных камней, что мне не хватает? Наконец, это может быть улучшено в целях эффективности?



Комментарии
3 ответа

Мне нравится ваш код. Вы можете обновить некоторые биты:

Области enumс

Лучше использовать ограниченные перечисления. Они объявляются с немного другой синтаксис:

enum class Seed { ... }; // enum struct Seed is also valid

Они приносят дополнительную безопасность, потому что они не могут быть неявно преобразованы к своему базовому типу (который можно указать, кстати: enum class Seed : char { ... };).

Вы можете затем использовать enumзначения как константы:

switch (seed_type) {
case Seed::Clock : ...
case Seed::Device : ...
case Seed::Value : ...
case Seed::Seq : ...
}

Я считаю, что указывать объем (в данном случае Seed::) позволяет более лаконичным достоинством, чем те макро-просмотр верхнего регистра имена констант, который вы выбрали.

Универсальных ссылок и точная пересылка

Вот несколько технических темой вы можете ознакомиться в статьях и блогах, но только так вы знаете основы, в контексте шаблона вычет, двойной амперсанд суффикс позволить компилятору вывести правильный способ передачи аргументов для вас:

// original code: Params are copied. 
// But what if they're big objects, or if they must keep track of a state?
template<class... Params>
explicit Generator( Engine engine, Params... params ) : _engine( engine ) {
_distribution = Distribution<Type>( params... );
}

// suggested modification:
// the compiler will correctly copy rvalues and pass lvalues by reference
template<class... Params>
explicit Generator( Engine engine, Params&&... params ) : _engine( engine ) {
_distribution = Distribution<Type>( std::forward<Params>(params)... );
}

Интерфейс усовершенствована

Я не проверял ваш код, но кажется, что ваш seed функция-член обязывает клиента для указания начального значения, даже если они только хотят использовать последовательность параметров. Вы должны иметь несколько seed функции с различными перегрузками или шаблонный seed функции, или комбинацию обоих.

Пример того, что вы могли бы сделать:

template <SeedType seed, typename Arg = int>
void seed(Arg&& arg = Arg{}) {
if constexpr (seed == USE_CHRONO_CLOCK) {
_engine.seed( getTimeNow() );
} else if constexpr (seed == USE_RANDOM_DEVICE) {
std::random_device device{};
_engine.seed( device() );
} else if constexpr (seed == USE_SEED_VALUE) {
_engine.seed( arg);
} else if constexpr (seed == USE_SEED_SEQ) {
std::seed_seq seq( arg );
_engine.seed( seq );
}
}

И тогда вы бы написали:

g1.seed<USE_RANDOM_DEVICE>();
// ...
g2.seed<USE_SEED_SEQ>(list2);
// ...
g3.seed<USE_CHRONO_CLOCK>();

Конечно, было бы лучше добавить несколько статических утверждений для проверки типа аргумента шаблона.

Различные

Брекеты лишнего вокруг ветвей switch заявление, и они на самом деле не повышают читабельность.

5
ответ дан 26 марта 2018 в 01:03 Источник Поделиться

Несколько замечаний.

Видимость

Есть ли специальный план в объявлении Clock публичным тип? Он не используется в любой другой части интерфейса. В настоящее время его можно использовать вряд ли что-нибудь более для пользователя, может сказать, какие часы используются для chronoinitialization с помощью функции std::is_same.

В то же время, Distribution<Type> это оставили как есть Distribution будучи аргумента шаблона, и это даже не typedefed везде внутри класса. Таким образом, пользователи не могут ввести что-то вроде

MyGenerator::Distribution d = generator.getDistribution();

они должны помнить, что Distribution для MyGenerator изначально, или использовать auto.

Инициализация

Разделение объекта строительства и вводу в два этапа-это вообще антипаттерн (да). В вашей реализации, Generator() и seed() оба обеспечивают уникальные способы запуска двигателя. Имело бы смысл добавить высева, связанные с возможностями конструктора; и чтобы по умолчанию значение семени в seed().

Перегрузка

seed() в своем нынешнем виде является слишком сложным. Процедура, которой только заявление switch скорее всего подходит для разделения на несколько перегрузов. Кроме того, он не предусматривает всех возможных способов запуска двигателя.

Возвращаемые значения

Положить seed в сторону, generate() возвращение...void? Это ОК, чтобы сохранить сгенерированные значения для позже повторить, но требует, чтобы пользователь сделать два звонки по цене одного? Действительно Type generate() { return _value = _distribution( _engine ); } кажется более полезным. Люди часто хотят случайные значения; это гораздо реже, что они просто хотят, чтобы произвести значение, и не заботиться о том, что он, наконец, оказался.

Слишком много копий

Например, engine передается по значению в конструктор и затем скопировал снова в _engine.

5
ответ дан 26 марта 2018 в 02:03 Источник Поделиться

Недавно был подобный вопрос. Одна основная суть заключается в том, что вы не должны добавить случайные устройства в качестве члена

В C++ Генерация Случайных Чисел

В конце концов, вы тоже не очень нужна после того, как вы инициализировали ваш двигатель.
Следовательно, вы должны только поставить двигатель В вы класса.

0
ответ дан 26 марта 2018 в 12:03 Источник Поделиться