8-15 全局随机数(Random.h)

如果要在多个函数或文件中使用随机数生成器该怎么办?一种方法是在 main() 函数中创建(并初始化)伪随机数生成器,然后将其传递到所有需要的地方。但这种做法需要频繁传递对象,而实际使用场景可能只是零星出现且分布在不同位置,会给代码带来大量冗余。

另一种方案是在每个需要随机数生成的函数中创建静态局部变量 std::mt19937(静态确保仅初始化一次)。但让每个使用随机数生成的函数都定义并初始化独立的局部生成器实属过度设计,且每个生成器的调用频率较低可能导致生成结果质量下降。

我们真正需要的是一个可共享的PRNG对象,能在所有函数和文件中任意调用。最佳方案是在命名空间内创建全局随机数生成器对象。还记得我们强调要避免使用非const全局变量吗?此处是个例外。

以下提供一个仅需头文件的简易方案,可在任何需要访问随机化自种子std::mt19937的代码文件中#include:

Random.h:

#ifndef RANDOM_MT_H
#define RANDOM_MT_H

#include <chrono>
#include <random>

// This header-only Random namespace implements a self-seeding Mersenne Twister.
// Requires C++17 or newer.
// It can be #included into as many code files as needed (The inline keyword avoids ODR violations)
// Freely redistributable, courtesy of learncpp.com (https://www.learncpp.com/cpp-tutorial/global-random-numbers-random-h/)
namespace Random
{
	// Returns a seeded Mersenne Twister
	// Note: we'd prefer to return a std::seed_seq (to initialize a std::mt19937), but std::seed can't be copied, so it can't be returned by value.
	// Instead, we'll create a std::mt19937, seed it, and then return the std::mt19937 (which can be copied).
	inline std::mt19937 generate()
	{
		std::random_device rd{};

		// Create seed_seq with clock and 7 random numbers from 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 };
	}

	// Here's our global std::mt19937 object.
	// The inline keyword means we only have one global instance for our whole program.
	inline std::mt19937 mt{ generate() }; // generates a seeded std::mt19937 and copies it into our global object

	// Generate a random int between [min, max] (inclusive)
        // * also handles cases where the two arguments have different types but can be converted to int
	inline int get(int min, int max)
	{
		return std::uniform_int_distribution{min, max}(mt);
	}

	// The following function templates can be used to generate random numbers in other cases

	// See https://www.learncpp.com/cpp-tutorial/function-template-instantiation/
	// You can ignore these if you don't understand them

	// Generate a random value between [min, max] (inclusive)
	// * min and max must have the same type
	// * return value has same type as min and max
	// * Supported types:
	// *    short, int, long, long long
	// *    unsigned short, unsigned int, unsigned long, or unsigned long long
	// Sample call: Random::get(1L, 6L);             // returns long
	// Sample call: Random::get(1u, 6u);             // returns unsigned int
	template <typename T>
	T get(T min, T max)
	{
		return std::uniform_int_distribution<T>{min, max}(mt);
	}

	// Generate a random value between [min, max] (inclusive)
	// * min and max can have different types
        // * return type must be explicitly specified as a template argument
	// * min and max will be converted to the return type
	// Sample call: Random::get<std::size_t>(0, 6);  // returns std::size_t
	// Sample call: Random::get<std::size_t>(0, 6u); // returns std::size_t
	// Sample call: Random::get<std::int>(0, 6u);    // returns int
	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 的文件中并保存。可选操作:将 Random.h 添加到项目中。
  2. 在项目中需要生成随机数的任何 .cpp 文件中包含 Random.h:#include “Random.h”
  3. 调用 Random::get(min, max) 可生成介于 min 和 max 之间的随机数(包含边界值)。无需初始化或额外配置。

以下示例程序演示 Random.h 的多种用法:

main.cpp:

#include "Random.h" // defines Random::mt, Random::get(), and Random::generate()
#include <cstddef> // for std::size_t
#include <iostream>

int main()
{
	// We can call Random::get() to generate random integral values
	// If the two arguments have the same type, the returned value will have that same type.
	std::cout << Random::get(1, 6) << '\n';   // returns int between 1 and 6
	std::cout << Random::get(1u, 6u) << '\n'; // returns unsigned int between 1 and 6

        // In cases where we have two arguments with different types
        // and/or if we want the return type to be different than the argument types
        // We must specify the return type using a template type argument (between the angled brackets)
	// See https://www.learncpp.com/cpp-tutorial/function-template-instantiation/
	std::cout << Random::get<std::size_t>(1, 6u) << '\n'; // returns std::size_t between 1 and 6

	// If we have our own distribution, we can access Random::mt directly

	// Let's create a reusable random number generator that generates uniform numbers between 1 and 6
	std::uniform_int_distribution die6{ 1, 6 }; // for C++14, use std::uniform_int_distribution<> die6{ 1, 6 };
	for (int count{ 1 }; count <= 10; ++count)
	{
		std::cout << die6(Random::mt) << '\t'; // generate a roll of the die here
	}

	std::cout << '\n';

	return 0;
}

关于 Random.h 可选实现的几点说明

通常,在头文件中定义变量和函数会导致该头文件被多个源文件包含时违反单一定义规则(ODR)。但我们通过将 mt 变量及相关函数定义为内联形式,实现了在定义完全一致的前提下,允许重复定义而不违反 ODR。由于这些定义是通过 #include 引入头文件实现(而非手动编写或复制粘贴),可确保其完全一致。内联函数和变量被引入语言的主要目的,正是为了实现此类仅依赖头文件的功能。

相关内容
我们在第7.9节——内联函数与变量中介绍了内联函数和变量。

我们需要克服的另一个挑战在于如何初始化全局 Random::mt 对象,因为我们希望它具备自种子功能,这样就无需手动调用初始化函数也能确保其正常工作。初始化器必须是表达式。但初始化std::mt19937时,我们需要多个辅助对象(std::random_device和std::seed_seq),而这些对象必须以语句形式定义。此时辅助函数便派上用场。函数调用本身即为表达式,因此可将函数返回值用作初始化器。而在函数内部,我们可以自由组合所需的语句。由此,我们的 generate() 函数通过结合系统时钟与 std::random_device 生成并返回完全初始化的 std::mt19937 对象,该对象随后被用作全局 Random::mt 对象的初始化器。

posted @ 2026-02-28 15:20  游翔  阅读(1)  评论(0)    收藏  举报