1 - C++的随机数工具
本文将简要介绍如何在C++中生成随机数,包括使用旧C风格和使用C++<random>
库两种方式。顺便还编写一个UUID/GUID生成工具类来加深对该知识的理解。
C风格随机数生成器
在C++11之前,生成随机数的唯一方法就是使用C风格的srand()
和rand()
函数,接下来康康怎么使用它们。
首先是用于初始化随机数生成器的srand()
函数,它需要一个随机数种子来生成随机数序列:
srand(static_cast<unsigned int>(time(nullptr)));
其中,time()
定义在<ctime>
中,它返回系统时间,通常是系统纪元(起始时间)后的秒数。当前系统时间是高质量的随机数种子,因为它不会重复,生成的随机数序列也不会重复。
然后就能用rand()
来生成随机数了:
std::cout << rand() << std::endl;
如果想要生成指定范围内的随机数,可以这样写:
// 返回[min, max]间的随机数
int getRandom(int min, int max)
{
return static_cast<int>(rand() % (max + 1UL - min)) + min;
}
这里设置1UL
(不是1
)是为了防止算术溢出。在不同编译器下,生成随机数的最大值RAND_MAX
也可能不同(从 0x7fff 到 0x7fffffff 不等)。
总的来说,在C++11以后不建议使用C风格生成随机数,它没有灵活性,生成的也不是那么随机。
C++的<random>
库
C++随机数生成库可以用不同的算法和分布生成随机数。它有三大组件:引擎,引擎适配器 和 分布。其中,
- 引擎负责生成实际的随机数,并存储生成后续随机数的状态。
- 引擎适配器负责修改与它关联的引擎的结果。
- 分布决定了生成随机数的范围,以及它们在该范围内的数学分布方式。
随机数引擎
引擎负责生成实际的随机数,并存储生成后续随机数的状态。<random>
中提供了四种随机数生成引擎:
random_device
:基于硬件的随机数生成器(没有硬件的话会使用软件算法),生成随机数的质量由熵决定,可通过.entropy()
方法查看该类使用当前硬件生成随机数的熵。如果没有硬件,使用软件算法,它的熵为0。linear_congtuential_engine
:基于线性同余算法的伪随机数生成器,保存状态所需的内存最少,但生成随机数序列的质量不怎么高。mersenne_twister_engine
:基于梅森旋转算法的伪随机数生成器,生成的随机数质量最高,且速度快。subtract_with_carry_engine
:基于带进位减法算法的伪随机数生成器,质量不如梅森旋转算法的那个。
这里我们只使用random_device
和mersenne_twister_engine
。
random_device
是真正的随机数生成器,它通常用于给随机数引擎生成种子,因为它生成随机数很慢。下例是random_device
的简单使用:
random_device rnd;
cout << "熵: " << rnd.entropy() << endl;
cout << "最小值: " << rnd.min() << ", 最大值: " << rnd.max() << endl;
cout << "生成的随机数: " << rnd() << endl;
mersenne_twister_engine
的类定义如下:
template <class _Ty, size_t _Wx, size_t _Nx, size_t _Mx, size_t _Rx, _Ty _Px, size_t _Ux, _Ty _Dx,
size_t _Sx, _Ty _Bx, size_t _Tx, _Ty _Cx, size_t _Lx, _Ty _Fx>
class mersenne_twister_engine
可以发现十分复杂,有足足14个参数,而且我们也看不懂(除了数学领域大神)。不过不用担心,C++标准定义了一些预定义的随机数引擎:
using std::mt19937 = mersenne_twister_engine<unsigned int, 32, 624, 397, 31, 0x9908b0df, 11, 0xffffffff, 7,
0x9d2c5680, 15, 0xefc60000, 18, 1812433253>;
我们直接用std::mt19937
即可。
随机数引擎的适配器
引擎适配器负责修改与它关联的引擎的结果。C++定义了以下3个适配器模板:
template<class Engine, size_t p, size_t r>
class discard_block_engine {...}
丢弃关联引擎engine
生成的一些值,以生成随机数。其中,关联引擎engine
生成p
个数,适配器丢弃一些数,返回r
个数。
template<class Engine, size_t w, class UIntType>
class independent_bits_engine {...}
组合关联引擎Engine
生成的随机数,以生成具有给定位数w
的随机数。
template<class Engine, size_t k>
class shuffle_order_enfine {...}
生成和关联引擎Engine
一致的随机数,然后打乱顺序。
预定义的随机数引擎和适配器
上面说到,如果自己不是数学领域大神,就不要自定义这些随机数引擎,C++帮我们预定义了如下随机数生成器:
预定义生成器 | 类模板 |
---|---|
minstd_rand0 | linear_congruential_engine |
minstd_rand | linear_congruential_engine |
mt19937 | mersenne_twister_engine |
mt19937_64 | mersenne_twister_engine |
ranlux24_base | linear_congruential_engine |
ranlux48_base | substract_with_carry_engine |
ranlux24 | discard_block_engine |
ranlux48 | discard_block_engine |
knuth_b | shuffle_order_enfine |
default_random_engine | 编译器实现 |
随机数分布
分布是一个描述数字在特定范围内分布的数学公式,它可以和伪随机数引擎结合使用,从而决定生成随机数的分布情况。可用的分布有:
- 均匀分布:
uniform_int_distribution
,uniform_real_distribution
- 伯努利分布(根据离散概率分布生成随机布尔值):
bernoulli_distribution
,binomial_distribution
,geometric_distribution
,negative_binomial_distribution
。 - 泊松分布(根据离散概率分布生成随机非负整数):
poission_distribution
,exponential_distribution
,gamma_distribution
,weibull_distribution
,extreme_value_distribution
。 - 正态分布:
normal_distribution
,lognormal_distribution
,chi_squared_distribution
,cauthy_distribution
,fisher_f_distribution
,student_t_distribution
。 - 采样分布:
discrete_distribution
,piecewise_constant_distribution
,piecewise_linear_distribution
。
简单使用
接下来就可以用<random>
库生成随机数了。
首先要创建一个引擎实例,如果是基于软件的引擎(伪随机数),还需要定义分布。这里以std::mt19937
为例,不过在创建它前,还需要用std::random_device
生成它的种子:
std::random_device seeder;
// 如果seeder不是随机数生成器(没硬件), 就用time()
const auto seed = seeder.entropy() ? seeder() : time(nullptr);
std::mt19937 engine(static_cast<std::mt19937::result_type>(seed));
创建好引擎实例后,还需要定义它的分布。这里使用均匀整数分布,范围是1~99:
std::uniform_int_distribution<int> distribution(1, 99);
最后这样使用就行了:
std::cout << distribution(engine) << std::endl;
为了简便使用,可用std::bind()
将engine
作为distribution()
的第一个参数,然后尝试用std::generate()
将它填满一个10个元素的std::vector
:
auto generator = std::bind(distribution, engine);
std::vector<int> values(10);
std::generate(std::begin(values), std::end(values), generator);
for (auto i : values) { std::cout << i << " "; }
我们还可以将它封装到函数中:
// std::function()
void fillVector(vector<int>& values, const std::function<int()>& generator)
{
std::generate(std::begin(values), std::end(values), generator);
}
// 函数模板
template<typename T>
void fillVector(vector<int>& values, const T& generator)
{
std::generate(std::begin(values), std::end(values), generator);
}
// C++20简写函数模板
void fillVector(vector<int>& values, const auto& generator)
{
std::generate(std::begin(values), std::end(values), generator);
}
实现UUID
目前是为了解决ImGui同名控件控制冲突的问题而创建的:
// UUID.h
class UUID
{
public:
UUID();
long operator() () const { return m_UUID; }
private:
long m_UUID;
};
// UUID.cpp
static std::random_device s_seeder;
static auto seed = s_seeder.entropy() ? s_seeder() : time(nullptr);
static std::mt19937 s_engine(seed);
static std::uniform_int_distribution<long> s_distribution;
UUID::UUID()
: m_UUID(s_distribution(s_engine))
{
}
可以发现比较简单,后期可能会进行修改。
参考资料
- C++20高级编程