Представление равномерного распределения по диапазону произвольного типа перечисления

Я использую библиотеку утилит случайных чисел C++ во многих местах. Это может быть не совсем удобно (например, нет базового класса для произвольного распределения), но - я научился жить с этим.

Теперь мне нужно равномерно выбирать значения из перечислимого типа. Я знаю, что на SO уже есть вопрос:

генерация случайных чисел

однако, этот:

  1. Предполагается, что все значения перечисления являются смежными, то есть это не будет работать для

    enum Color { Red = 1, Green = 2, Blue = 4 }
    

    где мы хотим, чтобы каждое из этих трех значений было выбрано с вероятностью 1/3.

  2. Не обеспечивает функциональность std::uniform_distribution<>то есть он не работает со случайным движком, вы его пропускаете и так далее.

Очевидно, я не могу использовать std::uniform_int_distribution<Color>Если только по причине 1 выше. Что мне делать вместо этого?

Заметки:

  • Код должен быть универсальным, то есть тип enum будет параметром шаблона.
  • Поскольку, скорее всего, мне понадобятся некоторые инструменты для всего лишь грубого перечисления, вы можете предположить, что он у меня есть; просто изложите свое предположение в явном виде.
  • Конкретно, и если это поможет, предположим, что я использую Better Enums, что делает меня полностью укомплектованным всеми прибамбасами.
  • Если есть какой-то идиоматический способ сделать это без использования какого-либо подобного инструментария, это даст хороший ответ, но я сомневаюсь в этом.
  • C++ 11/14-только решения являются приемлемыми.
  • Несколько идентификаторов перечисления с одинаковым значением не удваивают частоту, они просто являются псевдонимами друг друга. Если у вас есть простое решение, предполагающее, что оно не существует, оно также будет уместным, хотя и неоптимальным.

4 ответа

Решение

Вот три реализации распределения, в порядке возрастания сложности:

Во-первых, если мы можем полагаться на то, что значения различны или все в порядке с переоценкой значений повтора, мы можем просто проиндексировать _values() контейнер:

template<class Enum>
struct SimpleEnumDistribution
{
    std::uniform_int_distribution<typename Enum::_integral> dist{0, Enum::_size() - 1};
    template<class Generator> Enum operator()(Generator& g) { return Enum::_values()[dist(g)]; }
};

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

template<class Enum>
struct UniformEnumDistribution
{
    std::uniform_int_distribution<typename Enum::_integral> dist{
        *std::min_element(Enum::_values().begin(), Enum::_values().end()),
        *std::max_element(Enum::_values().begin(), Enum::_values().end())};
    template<class Generator> Enum operator()(Generator& g)
    {
        for (;;)
            if (auto value = Enum::_from_integral_nothrow(dist(g)))
                return *value;
    }
};

Если это будет неэффективно (возможно, значения перечисления редки), мы можем вычислить таблицу поиска при инициализации:

template<class Enum>
struct FastUniformEnumDistribution
{
    std::uniform_int_distribution<std::size_t> dist;
    std::array<typename Enum::_integral, Enum::_size()> values;
    FastUniformEnumDistribution()
    {
        std::copy(Enum::_values().begin(), Enum::_values().end(), values.data());
        std::sort(values.begin(), values.end());
        dist.param(std::uniform_int_distribution<std::size_t>::param_type{0u, static_cast<std::size_t>(
            std::distance(values.begin(), std::unique(values.begin(), values.end())) - 1)});
    }
    template<class Generator> Enum operator()(Generator& g)
    {
        return Enum::_from_integral_unchecked(values[dist(g)]);
    }
};

Пример.

С использованием Better Enums эта проблема может быть решена следующим образом:

template<typename T>
typename T get_uniform_value(std::default_random_engine& eng)
{
    std::uniform_int_distribution<int> dist(0, T::_size() - 1);
    return T::_values()[dist(eng)];
}

Пример использования:

BETTER_ENUM(Channel, int, Red, Green = 2, Blue) // Enum to generate random values of
...
std::default_random_engine rng(std::random_device{}());
Channel r = get_uniform_value<Channel>(rng); // Uniformly distributed between 0, 2 and 3

Я бы сказал, что более идиоматичным будет создание массива и выбор индекса из массива:

 template <typename Rnd>
 Color RandomColor(Rnd& rnd)
 {
     const std::array<Color, 3u> colors {Color::Red, Color::Green, Color::Blue};

     std::uniform_int_distribution<int> dist(0, colors.size() - 1);
     return colors[dist(rnd)];
 }

Лучше Enums, кажется, позволяют не создавать массив вручную с Color::_values:

 template <typename BetterEnum, typename Rnd>
 BetterEnum RandomBetterEnum(Rnd& rnd)
 {
     std::uniform_int_distribution<int> dist(0, BetterEnum::_size() - 1);
     return BetterEnum::_values()[dist(rnd)];
 }

В вопросе, с которым вы связаны, предполагается, что вы хотите равномерное распределение по значениям перечислителя.

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

Есть и другие фундаментальные проблемы:

В том случае, если вы показали

enum Color { Red = 1, Green = 2, Blue = 4 }

Предположительно, вы хотите получить равномерное распределение от 0 до 7 (каждый перечислитель может ИЛИ использовать битовые маски).

Предположим, что перечисление было:

enum Color { Red = 1, Green = 2, Blue = 3 }

Тогда, по-видимому, вы хотите только 1, 2, 3 в вашем дистрибутиве.

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

Короче говоря, я думаю, что вы должны делать именно то, что делает вопрос, на который вы ссылаетесь, и создавать соответствующий дистрибутив intили что-то еще, а потом static_cast это к перечислению. И не пытайтесь использовать какое-либо шаблонное решение, которое пытается прочитать ваши мысли для каждого возможного перечисления.

Другие вопросы по тегам