Используя С++ 11 <random> header, каков правильный способ получить целое число от 0 до n?
Я только начинаю использовать заголовок С++ 11 <random>
в первый раз, но есть еще некоторые вещи, которые кажутся немного загадочными. Этот вопрос касается предполагаемого, идиоматического, наилучшего способа выполнения очень простой задачи.
В настоящее время в одной части моего кода у меня есть что-то вроде этого:
std::default_random_engine eng {std::random_device{}()};
std::uniform_int_distribution<> random_up_to_A {0, A};
std::uniform_int_distribution<> random_up_to_B {0, B};
std::uniform_int_distribution<> random_up_to_some_other_constant {0, some_other_constant};
а затем, когда мне нужно целое число от 0 до B, я вызываю random_up_to_B(eng)
.
Так как это начинает выглядеть немного глупо, я хочу реализовать функцию rnd
, так что rnd(n, eng)
возвращает случайное целое число от 0 до n.
Что-то вроде следующего должно работать
template <class URNG>
int rnd(int n, URNG &eng) {
std::uniform_int_distribution<> dist {0, n};
return dist(eng);
}
но это связано с созданием нового объекта распределения каждый раз, и у меня создается впечатление, что не так, как вы должны это делать.
Итак, мой вопрос в том, каков наилучший способ выполнения этой простой задачи, используя абстракции, предоставляемые заголовком <random>
? Я спрашиваю, потому что я обязан делать гораздо более сложные вещи, чем это позже, и я хочу убедиться, что я правильно использую эту систему.
Ответы
Ответ 1
uniform_int_distribution
не стоит дорого строить, поэтому каждый раз создавать новые ограничения должны быть в порядке. Однако есть способ использовать один и тот же объект с новыми ограничениями, но он громоздкий.
uniform_int_distribution::operator()
имеет перегрузку, которая принимает объект uniform_int_distribution::param_type
, который может указывать новые пределы, которые будут использоваться, но param_type
сам по себе является непрозрачным типом, и нет никакого переносного способа его создания, кроме извлечения его из существующего экземпляра uniform_int_distribution
. Например, для построения uniform_int_distribution::param_type
можно использовать следующую функцию.
std::uniform_int_distribution<>::param_type
make_param_type(int min, int max)
{
return std::uniform_int_distribution<>(min, max).param();
}
Передайте их в operator()
, и сгенерированный результат будет в указанном диапазоне.
Живая демонстрация
Итак, если вы действительно хотите повторно использовать один и тот же uniform_int_distribution
, создайте и сохраните несколько экземпляров param_type
, используя приведенную выше функцию, и используйте их при вызове operator()
.
Ответ выше неточен, поскольку стандарт указывает, что param_type
может быть сконструирован из тех же аргументов распределения, что и те, которые используются соответствующим конструктором типа рассылки. Благодаря @T.C. для указав это.
Из §26.5.1.6/9 [rand.req.dist]
Для каждого из конструкторов D
, принимающих аргументы, соответствующие параметрам распределения, P
должен иметь соответствующий конструктор, подчиненный тем же требованиям, и принимать аргументы, идентичные по числу, типу и значениям по умолчанию. ...
Таким образом, нам не нужно создавать объект распределения без необходимости только для извлечения param_type
. Вместо этого функция make_param_type
может быть изменена на
template <typename Distribution, typename... Args>
typename Distribution::param_type make_param_type(Args&&... args)
{
return typename Distribution::param_type(std::forward<Args>(args)...);
}
который можно использовать как
make_param_type<std::uniform_int_distribution<>>(0, 10)
Живая демонстрация
Ответ 2
Отвечая на мой собственный вопрос: адаптировав пример, найденный в этот документ, показано, что это правильный способ реализации функции, возвращающей случайное целое число от 0 до n-1 включительно:
template<class URNG>
int rnd(int n, URNG &engine) {
using dist_t = std::uniform_int_distribution<>;
using param_t = dist_t::param_type;
static dist_t dist;
param_t params{0,n-1};
return dist(engine, params);
}
Чтобы сделать его потокобезопасным, следует избегать объявления static
. Одна из возможностей состоит в том, чтобы сделать класс удобства по этим строкам, что я и использую в своем собственном коде:
template<class URNG>
class Random {
public:
Random(): engine(std::random_device{}()) {}
Random(typename std::result_of<URNG()>::type seed): engine(seed) {}
int integer(int n) {
std::uniform_int_distribution<>::param_type params {0, n-1};
return int_dist(engine, params);
}
private:
URNG engine;
std::uniform_int_distribution<> int_dist;
};
Это создается с помощью (например) Random<std::default_random_engine> rnd
, и тогда случайные целые числа могут быть получены с помощью rnd.integer(n)
. Методы выборки из других дистрибутивов могут быть легко добавлены в этот класс.
Чтобы повторить то, что я сказал в комментариях, повторное использование объекта распространения, вероятно, не нужно для конкретной задачи равномерных выборочных целых чисел, но для других дистрибутивов я думаю, что это будет более эффективно, чем создание его каждый раз, потому что есть некоторые алгоритмы для выборки из некоторых дистрибутивов, которые могут сохранять циклы CPU, генерируя одновременно несколько значений. (В принципе даже uniform_int_distribution
можно было бы это сделать, посредством векторизации SIMD.) Если вы не можете повысить эффективность за счет сохранения объекта распределения, тогда трудно представить, почему они разработали API таким образом.
Ура для С++ и его ненужная сложность! Это завершает дневную работу, выполняющую простую пятиминутную задачу, но по крайней мере у меня есть гораздо лучшая идея, что я делаю сейчас.
Ответ 3
Идиоматический способ генерации кода в соответствии с изменяющимися параметрами заключается в создании объектов распределения по мере необходимости, за Радиус действия равномерного_распределения:
std::random_device rd;
std::default_random_engine eng{rd()};
int n = std::uniform_int_distribution<>{0, A}(eng);
Если вы обеспокоены тем, что производительность может быть затруднена из-за невозможности полностью использовать внутреннее состояние распространения, вы можете использовать один дистрибутив и передавать ему разные параметры каждый раз:
std::random_device rd;
std::default_random_engine eng{rd()};
std::uniform_int_distribution<> dist;
int n = dist(eng, decltype(dist)::param_type{0, A});
Если это кажется сложным, считайте, что для большинства целей вы будете генерировать случайные числа в соответствии с тем же распределением с теми же параметрами (следовательно, конструктор распределения, принимающий параметры); путем изменения параметров, которые вы уже входите в расширенную территорию.