若需在多个函数或源文件中使用同一个随机数生成器,可有哪些做法?
一种方式是在 main()
中创建并播种 PRNG,然后将其作为参数传递到每一处需要的地方;但若使用地点零散,则会导致大量传参,令代码冗余。
另一种方式是在每个需要随机数的函数内部创建 static
局部 std::mt19937
(设为 static
仅播种一次)。然而,让每个函数各自定义并播种生成器既冗余,又可能因调用次数过少而降低随机质量。
我们真正需要的是:一个可在所有函数及源文件中共享访问的单一 PRNG 对象。最佳方案是创建一个位于命名空间内的全局随机数生成器。请记住,我们曾告诫避免非常量全局变量,但此处为例外。
下面给出一个仅需包含头文件即可使用的解决方案,可在任何需要随机、自播种 std::mt19937
的源文件中直接 #include
:
Random.h:
#ifndef RANDOM_MT_H
#define RANDOM_MT_H
#include <chrono>
#include <random>
// 本头文件仅含头文件形式的 Random 命名空间实现,采用自播种梅森旋转。
// 需 C++17 或更高版本。
// 可自由包含至任意数量的源文件(借助 inline 避免 ODR 冲突)。
// 由 learncpp.com 提供,可自由再分发。
namespace Random
{
// 返回一个已播种的 std::mt19937
// 注:本想返回 std::seed_seq,但其不可复制,故改为返回已播种的 std::mt19937。
inline std::mt19937 generate()
{
std::random_device rd{};
// 用系统时钟 + 7 个随机数构造 seed_seq
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() };
// 生成 [min, max] 范围内的随机整数(含端点)
// 可处理参数类型不同但可转换为 int 的情形
inline int get(int min, int max)
{
return std::uniform_int_distribution{ min, max }(mt);
}
// 下列函数模板用于其他场景,可忽略
// 生成 [min, max] 范围内的随机值
// 要求 min 与 max 同类型,返回同类型
// 支持:short、int、long、long long 及其无符号版本
// 示例:Random::get(1L, 6L); // 返回 long
template <typename T>
T get(T min, T max)
{
return std::uniform_int_distribution<T>{ min, max }(mt);
}
// 生成 [min, max] 范围内的随机值
// min 与 max 可不同类型,返回类型需显式指定
// min 与 max 将转换为返回类型
// 示例:Random::get<std::size_t>(0, 6);
template <typename R, typename S, typename T>
R get(S min, T max)
{
return get<R>(static_cast<R>(min), static_cast<R>(max));
}
}
#endif
使用 Random.h
仅需三步即可生成随机数:
- 将上述代码复制保存为项目目录下的
Random.h
。 - 在任何需要随机数的
.cpp
文件中#include "Random.h"
。 - 调用
Random::get(min, max)
获得 [min, max] 范围内的随机整数(含端点),无需任何初始化。
示例程序(main.cpp):
#include "Random.h"
#include <cstddef>
#include <iostream>
int main()
{
// 生成同类型随机整数
std::cout << Random::get(1, 6) << '\n'; // int
std::cout << Random::get(1u, 6u) << '\n'; // unsigned int
// 不同类型或需指定返回类型时使用模板实参
std::cout << Random::get<std::size_t>(1, 6u) << '\n';
// 也可直接使用 Random::mt 配合自定义分布
std::uniform_int_distribution die6{ 1, 6 };
for (int i = 0; i < 10; ++i)
std::cout << die6(Random::mt) << '\t';
std::cout << '\n';
return 0;
}
关于 Random.h 的实现说明(可选阅读)
通常,在头文件中定义变量与函数会违反一次定义规则(ODR)。然而,我们将 mt
及相关函数设为 inline
,只要所有定义完全一致,即可重复包含而不违反 ODR。借助头文件包含,可确保定义一致性。
相关内容参见 7.9 — 内联函数与变量。
另一挑战是如何在全局作用域以表达式初始化 Random::mt
。初始化表达式需为单表达式,而构造 std::mt19937
需若干辅助对象(std::random_device
、std::seed_seq
),它们必须作为语句定义。借助辅助函数即可解决:函数调用是表达式,可在函数内部完成复杂逻辑。generate()
函数创建并返回已播种的 std::mt19937
,再用于初始化全局对象。