全局随机数(Random.h)

若需在多个函数或源文件中使用同一个随机数生成器,可有哪些做法?
一种方式是在 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

仅需三步即可生成随机数:

  1. 将上述代码复制保存为项目目录下的 Random.h
  2. 在任何需要随机数的 .cpp 文件中 #include "Random.h"
  3. 调用 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_devicestd::seed_seq),它们必须作为语句定义。借助辅助函数即可解决:函数调用是表达式,可在函数内部完成复杂逻辑。generate() 函数创建并返回已播种的 std::mt19937,再用于初始化全局对象。

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"