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_devicemersenne_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_rand0linear_congruential_engine
minstd_randlinear_congruential_engine
mt19937mersenne_twister_engine
mt19937_64mersenne_twister_engine
ranlux24_baselinear_congruential_engine
ranlux48_basesubstract_with_carry_engine
ranlux24discard_block_engine
ranlux48discard_block_engine
knuth_bshuffle_order_enfine
default_random_engine编译器实现

随机数分布

分布是一个描述数字在特定范围内分布的数学公式,它可以和伪随机数引擎结合使用,从而决定生成随机数的分布情况。可用的分布有:

  • 均匀分布:uniform_int_distributionuniform_real_distribution
  • 伯努利分布(根据离散概率分布生成随机布尔值):bernoulli_distributionbinomial_distributiongeometric_distributionnegative_binomial_distribution
  • 泊松分布(根据离散概率分布生成随机非负整数):poission_distributionexponential_distributiongamma_distributionweibull_distributionextreme_value_distribution
  • 正态分布:normal_distributionlognormal_distributionchi_squared_distributioncauthy_distributionfisher_f_distributionstudent_t_distribution
  • 采样分布:discrete_distributionpiecewise_constant_distributionpiecewise_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高级编程