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>
// To tylko nagłówek Losowa przestrzeń nazw implementuje samozasysający Mersenne Twister.
// Wymaga C++17 lub nowszy.
// Można #włączyć do dowolnej liczby plików z kodem (słowo kluczowe inline pozwala uniknąć naruszeń ODR)
// Freely redistributable, courtesy of learncpp.com (/www.learncpp.com/cpp-tutorial/global-random-numbers-random-h/)
namespace Random
{
// Zwraca zaszczepionego Mersenne Twistera
// Uwaga: wolelibyśmy zwrócić std::seed_seq (aby zainicjować std::mt19937), ale std::seed nie może zostać skopiowane, więc nie może zostać zwrócone przez wartość.
// Zamiast tego utworzymy std::mt19937, zaszczepimy go, a następnie zwrócimy std::mt19937 (który można skopiować).
inline std::mt19937 generate()
{
std::random_device rd{};
// Utwórz seq_seq z zegarem i 7 losowymi liczbami z 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 };
}
// Oto nasz globalny obiekt std::mt19937.
// Wbudowane słowo kluczowe oznacza, że mamy tylko jedną globalną instancję dla całego programu.
inline std::mt19937 mt{ generate() }; // zarazi się std::mt19937 i kopiuje go do naszego obiektu globalnego
// Wygeneruj losową wartość int pomiędzy [min, max] (włącznie)
// * obsługuje również przypadki, gdy dwa argumenty mają różne typy, ale można je przekonwertować na int
inline int get(int min, int max)
{
return std::uniform_int_distribution{min, max}(mt);
}
// Następujące szablony funkcji mogą być użyte do generowania liczb losowych w innych przypadkach
// See /www.learncpp.com/cpp-tutorial/function-template-instantiation/
// Możesz je zignorować, jeśli ich nie rozumiesz
// Wygeneruj losową wartość pomiędzy [min, max] (włącznie)
// * min i max muszą mieć wartość tego samego typu
// * wartość zwracana ma ten sam typ co min i max
// * Supported types:
// * krótki, całkowity, długi, długi, długi
// * unsigned krótki, unsigned int, unsigned long lub unsigned long long
// Przykładowe wywołanie: Losowo::get(1L, 6L); // zwraca long
// Przykładowe wywołanie: Random::get(1u, 6u); // zwraca unsigned int
template <typename T>
T get(T min, T max)
{
return std::uniform_int_distribution<T>{min, max}(mt);
}
// Wygeneruj losową wartość pomiędzy [min, max] (włącznie)
// * min i max mogą mieć różne typy
// * typ zwracany musi być jawnie określony jako argument szablonu
// * min i max zostaną skonwertowane na typ zwracany
// Przykładowe wywołanie: Random::get<std::size_t>(0, 6); // zwraca std::size_t
// Przykładowe wywołanie: Random::get<std::size_t>(0, 6u); // zwraca std::size_t
// Przykładowe wywołanie: Random::get<std::int>(0, 6u); // zwraca 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" // określenie Random::mt, Random::get() i Random::generate()
#include <cstddef> // dla std::size_t
#include <iostream>
int main()
{
// Możemy wywołać Random::get() w celu wygenerowania losowych wartości całkowitych
// Jeśli oba argumenty mają ten sam typ, zwrócona wartość będzie miała ten sam typ.
std::cout << Random::get(1, 6) << '\n'; // zwraca wartość int pomiędzy 1 a 6
std::cout << Random::get(1u, 6u) << '\n'; // zwraca wartość int bez znaku pomiędzy 1 a 6
// W przypadkach, gdy mamy dwa argumenty różnych typów
// i/lub jeśli chcemy, aby typ zwracany był inny niż typy argumentów
// Musimy określić typ zwracany za pomocą argumentu typu szablonu (między nawiasami ostrymi)
// See /www.learncpp.com/cpp-tutorial/function-template-instantiation/
std::cout << Random::get<std::size_t>(1, 6u) << '\n'; // zwraca std::size_t od 1 do 6
// Jeśli mamy własną dystrybucję, możemy uzyskać dostęp Random::mt bezpośrednio
// Utwórzmy generator liczb losowych wielokrotnego użytku, który generuje liczby jednolite od 1 do 6
std::uniform_int_distribution die6{ 1, 6 }; // dla C++14 użyj std::uniform_int_distribution<> die6{ 1, 6 };
for (int count{ 1 }; count <= 10; ++count)
{
std::cout << die6(Random::mt) << '\t'; // wygeneruj tutaj rzut kostką
}
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ć std::mt19937, potrzebujemy kilku obiektów pomocniczych (std::random_device oraz 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.

