如何使用 C++20 正确实现生成伪随机整数的函数
How to correctly implement a function that will generate pseudo-random integers with C++20
我想指出,在 C++ 中,伪随机数的生成过于复杂。如果你还记得像 Pascal 这样的古老语言,那么它们有函数 Random(n)
,其中 n
是整数,生成范围是从 0
到 n-1
。现在,回到现代 C++,我想得到一个类似的接口,但有一个函数 random_int(a,b)
,它在 [a,b]
.
中生成数字
考虑以下示例:
#include <random>
namespace utils
{
namespace implementation_details
{
struct eng_wrap {
std::mt19937 engine;
eng_wrap()
{
std::random_device device;
engine.seed(device());
}
std::mt19937& operator()()
{
return engine;
}
};
eng_wrap rnd_eng;
}
template <typename int_t, int_t a, int_t b> int_t random_int()
{
static_assert(a <= b);
static std::uniform_int_distribution<int_t> distr(a, b);
return distr(implementation_details::rnd_eng());
}
}
可以看到distr
是用static
关键字标记的。因此,使用相同的参数重复调用将不会导致类型 std::uniform_int_distribution
.
的构造
在某些情况下,在编译时我们不知道世代边界。
因此,我们不得不重写这个函数:
template <typename int_t> int_t random_int2(int_t a, int_t b)
{
std::uniform_int_distribution<int_t> distr(a, b);
return distr(implementation_details::rnd_eng());
}
接下来,假设这个函数的第二个版本被调用了更多次:
int a, b;
std::cin>>a>>b;
for (int i=1;i!=1000000;++i)
std::cout<<utils::random_int2(a,b)<<' ';
问题
- 在每个中创建
std::uniform_int_distribution
的成本是多少
循环的迭代?
- 你能建议一个更优化的函数吗?returns一个普通桌面应用程序在传递范围内的伪随机数?
IMO,对于大多数简单的程序,例如游戏、图形和 Monte Carlo 模拟,您实际上 想要 的 API 是
static xoshiro256ss g;
// Generate a random number between 0 and n-1.
// For example, randint0(2) flips a coin; randint0(6) rolls a die.
int randint0(int n) {
return g() % n;
}
// This version is useful for games like NetHack, where you often
// want to express an ad-hoc percentage chance of something happening.
bool pct(int n) {
return randint0(100) < n;
}
(或用 std::mt19937
代替 xoshiro256ss
但请注意,您是在牺牲性能来换取...某些东西。:))
上面的 % n
在数学上是可疑的,当 n
是天文数字时(例如,如果你正在滚动 12297829382473034410 面的骰子,你会发现 0 和 6148914691236517205 之间的值出现两倍的频率)。所以你可能更喜欢使用 C++11 的 uniform_int_distribution
:
int randint0(int n) {
return std::uniform_int_distribution<int>(0, n-1)(g);
}
但是,请再次注意,您正在以原始速度为代价获得数学上的完美。 uniform_int_distribution
更适用于当您不相信您的随机数引擎是正常的(例如,如果引擎的输出范围可能是 0 到 255,但您想要生成 1 到 1000 的数字),或者当您'重新编写模板代码以处理任意整数分布(例如 binomial_distribution
、geometric_distribution
),并且需要一个具有相同一般“形状”的均匀分布对象来插入您的模板。
第 1 个问题的答案是“免费”。通过将 std::uniform_int_distribution<int>(0, n-1)
的结果存储到静态变量中,您将 而不是 获得任何东西。分发对象非常小,可简单复制,并且基本上可以自由构建。事实上,在这种情况下构造 uniform_int_distribution
的成本比线程安全静态初始化的成本 便宜 个数量级。
(有一些特殊情况,例如 std::normal_distribution
,其中在调用之间不隐藏分发对象可能导致您做的工作量是需要的两倍;但 uniform_int_distribution
不是这些情况之一.)
如果您想重复使用相同的 a
和 b
,请使用带有成员函数的 class — 这就是它们是为了。如果您不想公开您的 rnd_eng
(选择排除有用的 多线程 客户端),请编写 class 以使用它:
template<class T>
struct random_int {
random_int(T a,T b) : d(a,b) {}
T operator()() const {return d(implementation_details::rnd_eng());}
private:
std::uniform_int_distribution<T> d;
};
我想指出,在 C++ 中,伪随机数的生成过于复杂。如果你还记得像 Pascal 这样的古老语言,那么它们有函数 Random(n)
,其中 n
是整数,生成范围是从 0
到 n-1
。现在,回到现代 C++,我想得到一个类似的接口,但有一个函数 random_int(a,b)
,它在 [a,b]
.
考虑以下示例:
#include <random>
namespace utils
{
namespace implementation_details
{
struct eng_wrap {
std::mt19937 engine;
eng_wrap()
{
std::random_device device;
engine.seed(device());
}
std::mt19937& operator()()
{
return engine;
}
};
eng_wrap rnd_eng;
}
template <typename int_t, int_t a, int_t b> int_t random_int()
{
static_assert(a <= b);
static std::uniform_int_distribution<int_t> distr(a, b);
return distr(implementation_details::rnd_eng());
}
}
可以看到distr
是用static
关键字标记的。因此,使用相同的参数重复调用将不会导致类型 std::uniform_int_distribution
.
在某些情况下,在编译时我们不知道世代边界。 因此,我们不得不重写这个函数:
template <typename int_t> int_t random_int2(int_t a, int_t b)
{
std::uniform_int_distribution<int_t> distr(a, b);
return distr(implementation_details::rnd_eng());
}
接下来,假设这个函数的第二个版本被调用了更多次:
int a, b;
std::cin>>a>>b;
for (int i=1;i!=1000000;++i)
std::cout<<utils::random_int2(a,b)<<' ';
问题
- 在每个中创建
std::uniform_int_distribution
的成本是多少 循环的迭代? - 你能建议一个更优化的函数吗?returns一个普通桌面应用程序在传递范围内的伪随机数?
IMO,对于大多数简单的程序,例如游戏、图形和 Monte Carlo 模拟,您实际上 想要 的 API 是
static xoshiro256ss g;
// Generate a random number between 0 and n-1.
// For example, randint0(2) flips a coin; randint0(6) rolls a die.
int randint0(int n) {
return g() % n;
}
// This version is useful for games like NetHack, where you often
// want to express an ad-hoc percentage chance of something happening.
bool pct(int n) {
return randint0(100) < n;
}
(或用 std::mt19937
代替 xoshiro256ss
但请注意,您是在牺牲性能来换取...某些东西。:))
上面的 % n
在数学上是可疑的,当 n
是天文数字时(例如,如果你正在滚动 12297829382473034410 面的骰子,你会发现 0 和 6148914691236517205 之间的值出现两倍的频率)。所以你可能更喜欢使用 C++11 的 uniform_int_distribution
:
int randint0(int n) {
return std::uniform_int_distribution<int>(0, n-1)(g);
}
但是,请再次注意,您正在以原始速度为代价获得数学上的完美。 uniform_int_distribution
更适用于当您不相信您的随机数引擎是正常的(例如,如果引擎的输出范围可能是 0 到 255,但您想要生成 1 到 1000 的数字),或者当您'重新编写模板代码以处理任意整数分布(例如 binomial_distribution
、geometric_distribution
),并且需要一个具有相同一般“形状”的均匀分布对象来插入您的模板。
第 1 个问题的答案是“免费”。通过将 std::uniform_int_distribution<int>(0, n-1)
的结果存储到静态变量中,您将 而不是 获得任何东西。分发对象非常小,可简单复制,并且基本上可以自由构建。事实上,在这种情况下构造 uniform_int_distribution
的成本比线程安全静态初始化的成本 便宜 个数量级。
(有一些特殊情况,例如 std::normal_distribution
,其中在调用之间不隐藏分发对象可能导致您做的工作量是需要的两倍;但 uniform_int_distribution
不是这些情况之一.)
如果您想重复使用相同的 a
和 b
,请使用带有成员函数的 class — 这就是它们是为了。如果您不想公开您的 rnd_eng
(选择排除有用的 多线程 客户端),请编写 class 以使用它:
template<class T>
struct random_int {
random_int(T a,T b) : d(a,b) {}
T operator()() const {return d(implementation_details::rnd_eng());}
private:
std::uniform_int_distribution<T> d;
};