Co się stanie, jeśli będziemy chcieli użyć generatora liczb losowych w wielu funkcjach lub plikach? Jednym ze sposobów jest utworzenie (i zaszczepienie) naszego PRNG w naszej main() funkcji, a następnie przekazanie go wszędzie tam, gdzie jest to potrzebne. Ale to dużo, jak na coś, czego możemy używać sporadycznie i w wielu różnych miejscach. Przekazywanie takiego obiektu spowodowałoby sporo bałaganu w naszym kodzie.
Alternatywnie możesz utworzyć statyczną zmienną lokalną std::mt19937 w każdej funkcji, która jej potrzebuje (statyczna, dzięki czemu zostanie zaszczepiona tylko raz). Jednak definiowanie i uruchamianie własnego lokalnego generatora dla każdej funkcji korzystającej z generatora liczb losowych jest przesadą, a mała liczba wywołań do każdego generatora może prowadzić do wyników o niższej jakości.
Naprawdę potrzebujemy pojedynczego obiektu PRNG, który możemy udostępniać i uzyskiwać do niego dostęp w dowolnym miejscu, we wszystkich naszych funkcjach i plikach. Najlepszą opcją jest utworzenie globalnego obiektu generatora liczb losowych (w przestrzeni nazw!). Pamiętasz, jak mówiliśmy, żebyś unikał zmiennych globalnych innych niż stałe? To wyjątek.
Oto proste rozwiązanie zawierające tylko nagłówek, które można #zamieścić w dowolnym pliku kodu, który wymaga dostępu do losowego, samozasiana std::mt19937:
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 (/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 /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));
}
}
#endifUsing Random.h
Generowanie liczb losowych przy użyciu powyższego jest tak proste, jak wykonanie tych trzech kroków:
- Skopiuj/wklej powyższy kod do pliku o nazwie
Random.hw katalogu projektu i zapisz go. Opcjonalnie dodaj Random.h do swojego projektu. #include "Random.h"z dowolnego pliku .cpp w swoim projekcie, który musi generować liczby losowe.- Wywołaj
Random::get(min, max)aby wygenerować losową liczbę z zakresuminimax(włącznie). Nie jest wymagana żadna inicjalizacja ani konfiguracja.
Oto przykładowy program demonstrujący różne zastosowania 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 /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;
}Kilka uwag na temat implementacji Random.h Opcjonalne
Zwykle zdefiniowanie zmiennych i funkcji w pliku nagłówkowym spowodowałoby naruszenie reguły jednej definicji (ODR), gdy ten plik nagłówkowy zostałby zawarty w więcej niż jednym pliku źródłowym. Jednakże stworzyliśmy nasze mt zmienne i funkcje pomocnicze inline, co pozwala nam na powielanie definicji bez naruszania ODR, o ile wszystkie te definicje są identyczne. Ponieważ #uwzględniamy te definicje z pliku nagłówkowego (zamiast wpisywać je ręcznie lub kopiować/wklejać), możemy zapewnić, że są identyczne. Funkcje i zmienne wbudowane zostały dodane do języka głównie po to, aby umożliwić tego rodzaju funkcjonalność obejmującą tylko nagłówek.
Powiązana treść
Funkcje i zmienne wbudowane omówimy na lekcji 7.9 -- Funkcje wbudowane i zmienne.
Innym wyzwaniem, któremu musimy sprostać, jest sposób inicjalizacji naszego obiektu globalnego Random::mt , ponieważ chcemy, aby był on samozasysający, abyśmy nie musieli pamiętać o jawnym wywołaniu funkcji inicjującej, aby działał poprawnie. Nasz inicjator musi być wyrażeniem. Aby jednak zainicjować a std::mt19937, potrzebujemy kilku obiektów pomocniczych (a std::random_device oraz a std::seed_seq), które muszą być zdefiniowane jako instrukcje. Tutaj z pomocą przychodzi funkcja pomocnicza. Wywołanie funkcji jest wyrażeniem, dlatego możemy użyć wartości zwracanej przez funkcję jako inicjatora. Wewnątrz samej funkcji możemy mieć dowolną kombinację instrukcji, których potrzebujemy. Zatem nasza generate() funkcja tworzy i zwraca w pełni obsadzony std::mt19937 obiekt (zaszczepiony przy użyciu zarówno zegara systemowego, jak i std::random_device), którego używamy jako inicjatora naszego globalnego Random::mt obiektem.

