Генераторы и дистрибутивы 2.0


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


Пользователь - bipll: https://codereview.stackexchange.com/users/159614/bipll

  • Видимость
  • Инициализация
  • Перегрузка
  • Возвращаемые Значения
  • Слишком Много Копий

Пользователь - papagaga: https://codereview.stackexchange.com/users/157553/papagaga

  • Распространяется Перечислений
  • Универсальных Ссылок И Точная Пересылка
  • Интерфейс Усовершенствована
  • Различные

Рефакторинг Класса

#ifndef GENERATOR_H
#define GENERATOR_H

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

enum class Seed { SeedValue, ChronoClock, RandomDevice, SeedSeq, UserDefined };

template<class Engine, class Type, template<typename> class Distribution>
class Generator {
public:
    typedef class Distribution<Type> Distribution;

private:
    Engine _engine;
    Distribution _distribution;
    Type _value;

public:    
    template<class SeedValue, class... Params>
    explicit Generator( Engine&& engine, Seed seedType, SeedValue seedValue, Params&&... params ) : _engine( std::move( engine ) ) {
        _distribution = Distribution( std::forward<Params>( params )... );    
        seed( seedType, seedValue );
    }

    Type generate() {
        return _value = _distribution( _engine );
    }

    Distribution getDistribution() const {
        return _distribution;
    }

    Type lastGeneratedValue() const {
        return _value;
    }

private:
    template<class SeedValue = int>
    void seed( Seed seedType, SeedValue value = 0 ) {
        if( seedType == Seed::SeedValue )
            _engine.seed( value );
    }

    // This overload has to choose between one or the other based on type passed
    void seed( Seed seedType ) {
        if( seedType == Seed::ChronoClock )
            _engine.seed( getTimeNow() );

        if( seedType == Seed::RandomDevice ) {
            std::random_device rd{};
            _engine.seed( rd() );
        }
    }

    void seed( Seed seedType, std::initializer_list<std::size_t>&& list ) {
        if( seedType == Seed::SeedSeq ) { // expects initializer_list<size_t>
            std::seed_seq seq( std::move( list ) );
            _engine.seed( seq );
        }
    }

    // NOTE: Not sure if this is correct; or how one would call it from current constructor
    template<class Ret, class... Args>
    void seed( Seed seedType, Args&&... ) {
        if( seedType == Seed::UserDefined ) { // can be any user defined method, lambda function<>, etc.
            std::function<Ret( std::forward<Args>... )> func;
            _engine.seed( func );
        }
    }

    std::size_t getTimeNow() {
        using Clock = std::conditional_t<std::chrono::high_resolution_clock::is_steady,
            std::chrono::high_resolution_clock,
            std::chrono::steady_clock>;

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

#endif // !GENERATOR_H

Простое Использование

#include <iostream>
#include "Generator.h"

int main() {
    std::mt19937 engine;
    Generator<std::mt19937, int, std::uniform_int_distriubtion>
    gen( std::move( engine ), Seed::ChronoClock, 0, 1, 100 );

    for ( std::size_t i = 0; i < 100; i++ )
        std::cout << gen.generate() << '\n';

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

Обзор изменений, внесенных от предложения каждого пользователя

Пользователь - bipll:

Visibility: я переехал using Clock =... из общественной сферы класса и перешел конкретно к функции тела члена getTimeNow() который сейчас частный метод, а не публичное. Что касается typedef из Distribution<Type> я добавил публичных typedef в класс.

Initialization: меня больше нет в конструкции двигателя тип, который выбирается пользователем строящегося первого, то заставляя их придется явно вызывать семян функции. Функции семян больше нет публики он теперь частный и вызывается конструктор класса. Я объединил обе конструкции и инициализации в один процесс.

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

Return Values: прежде чем я разместил свой оригинальный вопрос и я уже хоть о том, создать функция возвращает значение, но я не включена, что, поскольку я был только тестирование класса по функциональности правильного конструирования и производства. Сейчас generate() метод возвращает значение. Что касается моего предыдущего метода, которые используют, чтобы получить сгенерированное значение, я переименовал ее, чтобы быть более понятной lastGeneratedValue().

Too many copies: Я сейчас включила механики для использования точной пересылки и / или семантику перемещения. Я теперь переместите engine в конструктор. Это также отражается на пользователей papagaga's раздел Universal References And Perfect Forwarding

Пользователь - papagaga:

Scoped Enumerations: я изменил мой enum быть class enum а также изменена области по делу от SOME_FIELD для SomeField.

Universal References And Perfect Forwarding: так же, как выше; использование ссылок и отличное пересылка сейчас я жду вариативных параметров в конструктор.

The Interface Is Perfectible: этот раздел в отражении перегрузки разделе усматривается из пользователей bipll. Я решил использовать перегруженные функции и шаблон функции перегрузки в случае необходимости. Я, однако, не включают каких-либо static asserts.

Various: это относилось к switch заявление подтяжки избыточное. Это не должно быть проблемой, т. к. я не долго с помощью switch заменить в функции перегрузки.


Вопросы - Проблемы

Хотелось бы узнать от пользователей, которые заложили прочную, хорошо детализированы и информативный ответ, если изменения, которые я сделал отражать то, что было предложено. Это точный код или все еще нуждается в улучшении?

А на мой следующий вопрос; он занимается seed() перегружается function template. Это должно позволить пользователю определять собственные function object для передачи семян функции семян генераторов или двигателей. Это может ответить любой, кто чувствует, что у них есть критические отзывы. Функция не проверены, код компилируется и бежать, не пытаясь использовать его. Тут syntax для объявления функции выглядит правильно? Если так, то вроде переполнения стека вопрос, но, как бы я интегрировать необходимый параметр(ы) для этой функции в этом классе конструктор? Этот конкретный перегрузки функция у меня держится.


Примечание - о мой оригинальный пост: я разделить и не могу выбрать между двумя пользователями упомянутых в этом посте, кто согласиться за принятый ответ; оба были чрезвычайно полезны. Я проголосовал за обоих пользователей ответы, но я до сих пор не определено, кто согласиться.



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

Об seed функцию, хотя, я должен предупредить вас, что вы можете быть удивлены, в какой-то момент, какие перегрузки компилятор выбрать. Если вы действительно знаете, что вы делаете, несколько перегруженные шаблоны, один из которых представляет тип по умолчанию, выглядит опасно. Если вы умножите перегрузок if constexpr в C++17-характеристика-это лучший способ пойти, потому что это дает вам гораздо больше контроля и гораздо больше гибкости, когда вы решите расширить свои функции.

Я посмотрел немного больше на новые генераторы случайных чисел в C++11 и интерфейс, которые они представляют. СТД двигателей' seed функция принимает либо значение или последовательность значений, как семя. Я понимаю, что вы хотите предложить другие варианты, но я боюсь, что вы делаете это за счет юзабилити (мне нужно помнить, что ваш seed функция имеет больше перегрузок, чем std'S, и мне нужно помнить также разных значений Seed перечислимый). Вы хотите сохранить ваш интерфейс ортогональных: что бы это значило здесь? Одна простая функция семян принимать либо значение или диапазон (допустим это ось Х) и столько функций, сколько нужно для создания указанного значения или диапазона (это наша ось Y).

x-axis: простой крюк к выбрали Engine::seed функция достаточно:

template <typename Seed_type>
void seed(Seed_type&& inp) {
_engine.seed(std::forward<Seed_type>(inp));
}

y-axis: вы можете предоставить упрощенный доступ к некоторым удобным семян-генератор функций, так что вы в конечном итоге вызов seed таким образом:

gen.seed(current_time()); // your getTimeNow() function
// or
gen.seed(random_number());
// of if user defined
gen.seed(my_seed_function()); // you don't even need to check if it returns
// a value or a sequence

Эти служебные функции могли быть статические функции, но это не очень удобно в значительной степени шаблонных классов. Можно группировать их в подпространство, чтобы сделать свою роль и свое происхождение более очевидным:

namespace seed {
std::size_t current_time();
std::size_t random_number();
// ...
}
// ...
gen.seed(seed::random_number());

Редактировать: я дал больше мыслей о том, как я бы оформил оберткой <random>. Я хотел бы начать с использования-случаях, и я считаю, что чаще всего одно: "мне нужен случайный дубль", или "мне нужны случайные числа между 1 и 6". Это приведет к:

template <typename T>
T random(T low = min<T>, T high = max<T>);
// ...
auto dice_roll = random<int>(1, 6);

где:

template <typename T>
constexpr T min = std::numeric_limits<T>::min();
template <typename T>
constexpr T max = std::numeric_limits<T>::max();

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

Главный смысл моей конструкции будет скрывать сложности. Я не хотел беспокоить моего клиента с семенами и распределения. Они хотели выбрать тип, и дополнительно различные, но не более того. Если они хотят полного контроля, они могут использовать <random> напрямую. Я беру на себя ответственность выбирать лучшим вариантом для большинства случаев, и я выбираю: 1) std::mt19937 считается самым лучшим многоборка двигателя, 2) равномерного распределения, что человек обычно ожидает, и 3) std::random_device была сделана, чтобы обеспечить семян.

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

Так вот что я бы сделал:

#include <iostream>
#include <random>
#include <limits>
#include <type_traits>

// only stylistic
template <typename T>
constexpr T min = std::numeric_limits<T>::min();
template <typename T>
constexpr T max = std::numeric_limits<T>::max();

template <typename T>
class Generator {
static_assert(std::is_arithmetic_v<T>); // <=> std::is_integral || std::is_floating_point

public:
using engine = std::mt19937;
using distribution = std::conditional_t<std::is_integral_v<T>,
std::uniform_int_distribution<T>,
std::uniform_real_distribution<T>>;
public:
Generator(T low = min<T>, T high = max<T>) :
_low(low),
_high(high),
_engine(std::random_device()()),
_distribution(low, high)
{}
T random() { return _distribution(_engine); }
void recalibrate(T low = min<T>, T high = max<T>) {
_low = low;
_high = high;
_engine.seed(std::random_device()());
_distribution = distribution(low, high);
}
private:
T _low, _high;
engine _engine;
distribution _distribution;
};

template <typename T>
T random(T low = min<T>, T high = max<T>) { // convenience function
Generator<T> gen(low, high);
gen.random();
}

int main() {
Generator<int> gen;
for (auto i = 0; i < 10; ++i) {
std::cout << gen.random() << '\n';
}
gen.recalibrate(20, 30);
std::cout << std::endl;
for (auto i = 0; i < 10; ++i) {
std::cout << gen.random() << '\n';
}
}

6
ответ дан 27 марта 2018 в 10:03 Источник Поделиться

Твое семя с определенными функция кажется неправильным. Первое, что вам не хватает скобки вокруг только первый оператор внутри блока if.

// NOTE: Not sure if this is correct; or how one would call it from current constructor
template<class Ret, class... Args>
void seed( Seed seedType, Args&&... ) {
if( seedType == Seed::UserDefined ) // can be any user defined method, lambda function<>, etc.
std::function<Ret( std::forward<Args>... )> func;
_engine.seed( func );
}

Где переданную функцию? Как правило, вы передаете его в качестве второго аргумента, а затем упаковать аргумент

template<class Ret, class... Args>
void seed( Seed seedType, Ret&& func, Args&&... ) {
if( seedType == Seed::UserDefined )
_engine.seed( func(std::forward<Args>... ));
}

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

Использование

Правило 80/20. 20% библиотеки используется в 80% кода. Оптимизировать для общего пользования. В случае использования, указанному в вопросе, библиотека может легко использовать шаблон псевдоним:

template <typename Engine, typename Integral>
using uniform_generator = Generator<Engine, Integral,
std::uniform_distribution<Integral>>;

Пример становится

uniform_generator<std::mt19937, int> gen{std::move(engine), Seed::ChronoClock, 0, 1, 100};
for ( std::size_t i = 0; i < 100; i++ )
std::cout << gen.generate() << '\n';

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

Еще немного долго, хотя.

Определить использование

Из примера, похоже, что основной сценарий использования-это поток случайных чисел или создать некоторые из них сразу. Пока интерфейс не позволит. Сделать интерфейсы легко использовать правильно и трудно использовать неправильно, Скотт Мейерс (не уверен, хотя если он был первым).

Неопределенное поведение

Документация упоминает о присутствии неопределенное поведение Type не один из перечисленных. Поставив по крайней мере static_assert было бы здорово.

Рассмотреть будущее

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

Слишком мелкие семена

Один 64-разрядный количество слишком мало, чтобы семя ~20к бит состояния. Я предпочитаю использовать три цифры.


  • duration_cast<nanoseconds>(high_resolution_clock::now()).count()

  • std::random_device{}()

  • количество вашему выбору. Мое 31

Все это бросили в std::seed_seq для устранения смещения.

Это намного сложнее, чем это может выглядеть

В общем, равномерное распределение не окончательное распределение. В зависимости от использования, другие распределения могут быть необходимы. Дискретные распределения может использоваться, чтобы динамически сбалансировать игру, перемещая вес. Нормальное распределение может быть использован для получения более простых типов врагов, NPC и в других случаях порождать один из специальных. Если вы уверены, что вам не понадобятся другие, то это замечательно.

Они не имеют много общего

Глядя На документации, понятно, что единственное, что их объединяет-это их operator() и общее значение семантики. Строительство и извлечения данных сильно отличаются, в сторону от их поведения. Как правило, лучше изготовить специальный код, который вы часто используете, и использовать другие непосредственно. Это очень трудно найти интерфейс, который будет соответствовать их всех.

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