章节目录

全局随机数

本节阅读量:

如果想在多个函数或文件中使用随机数生成器,会发生什么情况?一种方法是在main()函数中创建(并播种)PRNG,然后把它传递到所有需要它的地方。但这个对象可能只是偶尔使用,却需要在许多地方传来传去。这样的传递会给代码增加不少混乱。

或者,您可以在每个需要它的函数中创建静态局部std::mt19937变量(使用静态变量可以确保它只被播种一次)。然而,让每个使用随机数生成器的函数都定义并播种自己的本地生成器是多余的,而且每个生成器的调用次数较少,也可能导致结果质量较低。

我们真正想要的是一个单一的PRNG对象,它可以跨越所有函数和文件,在任何地方共享和访问。这里的最佳选择是创建全局随机数生成器对象。还记得我们曾建议避免非常量全局变量吗?这是一个例外。

下面是一个简单的、仅含头文件的解决方案,您可以将其包含在任何使用全局随机数的文件中:

random.h:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#ifndef RANDOM_MT_H
#define RANDOM_MT_H

#include <chrono>
#include <random>

// 只有头文件,内部的 Random 命名空间提供了播种好的Mersenne Twister PRNG的全局访问能力
// 可以在任意文件中直接引入使用 ( inline 关键字避免了单定义规则的报错)
namespace Random
{
	// 返回一个播种好的 Mersenne Twister
	inline std::mt19937 generate()
	{
		std::random_device rd{};

		// 返回一个时间戳以及由7个std::random_device产出的随机数组成的种子序列
		std::seed_seq ss{
			static_cast<std::seed_seq::result_type>(std::chrono::steady_clock::now().time_since_epoch().count()),
				rd(), rd(), rd(), rd(), rd(), rd(), rd() };

		return std::mt19937{ ss };
	}

	// 全局的 std::mt19937 对象.
	// inline 关键字,意味着该对象在整个程序中只有一个
	inline std::mt19937 mt{ generate() }; // 生成一个播种好的std::mt19937对象

	// 产出在 [min, max] 间的一个随机数
	inline int get(int min, int max)
	{
		return std::uniform_int_distribution{min, max}(mt);
	}
}

#endif

以及如何使用它的示例程序:

主.cpp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include "Random.h" // 引入 Random::mt, Random::get(), 以及 Random::generate()
#include <iostream>

int main()
{
	// 可以使用 Random::get() 去获得一个范围内的随机数

	std::cout << Random::get(1, 6) << '\n';   // 产出在1到6之间的随机数

	// 可以定义另外一个分布,使用全局的 Random::mt 来产出随机数

	// 创建一个新的随机数生成器,来均匀的生成1到6之间的数字
	std::uniform_int_distribution die6{ 1, 6 };

	// 打印一堆随机数
	for (int count{ 1 }; count <= 10; ++count)
	{
		// 可以直接访问 Random::mt
		std::cout << die6(Random::mt) << '\t'; // 投掷一次骰子,得到结果
	}

	std::cout << '\n';

	return 0;
}

通常,当头文件被包含到多个源文件中时,在头文件中定义变量和函数会导致违反单定义规则(ODR)。然而,这里已经将mt变量和相关函数声明为inline,只要这些重复定义完全相同,就不会违反ODR。因为我们使用#include引入头文件(而不是手动键入或复制/粘贴这些定义),所以可以确保它们是相同的。

我们必须克服的另一个挑战是如何初始化全局Random::mt对象,因为我们希望它能自动播种,这样就不必记住显式调用初始化函数。初始值设定项必须是表达式。为了初始化std::mt19937,需要几个辅助对象(std::random_device和std::seed_seq),而这些对象通常需要通过语句来定义。这就是辅助函数派上用场的地方。函数调用是表达式,因此可以使用函数的返回值作为初始值设定项。在函数中,可以包含所需的任意语句组合。因此,generate()函数会创建并返回一个完全播种的std::mt19937对象(使用系统时钟和std::random_device播种),用作全局Random::mt对象的初始值设定项。

一旦引用“Random.h”,就可以用以下两种方式之一使用它:

  1. 可以调用Random::get()在两个值之间生成一个随机数(包含两个端点)。
  2. 可以通过Random::mt直接访问std::mt19937对象,并对其执行任何操作。

8.13 使用Mersenne Twister生成随机数

上一节

8.15 第八章总结

下一节


本节目录