Dość często spotykam się z kodem wyglądającym w przybliżeniu tak:
int x = rand()%200-100; int y = rand()%200-100; |
Czy zauważyliście już błąd? Jeśli tak lub nie, to możliwe, że nie macie racji – nie da się z powyższego kodu wywnioskować jaka była intencja programisty – czy to miał być kod losujący wartość z przedziału [-100, 99] czy też [-100, 100].
Pomijając nieczytelność/niejawność zamiarów, rand ma jeszcze następujące problemy:
- nie musi być bezpieczny w wielowątkowości,
- RAND_MAX może wynosić zaledwie 32767,
- nawet jeśli sam rand wypluwa wszystkie liczby z jednakowym prawdopodobieństwem, modulo ten porządek zakłóca.
Ciężko nie wspomnieć o słynnym wykładzie “rand() Considered Harmful”: [yt][ch9]
Ok, to co robić?
Nie chcę/nie mogę używać dodatkowych nagłówków
<random> na ratunek. Wystarczy stworzyć prosty szablon:
thread_local std::mt19937 gen{std::random_device{}()}; template<typename T> T random(T min, T max) { return std::uniform_int_distribution<T>{min, max}(gen); } |
Można się też pokusić o ciut bardziej zaawansowaną wersję wspierającą również typy zmiennoprzecinkowe:
thread_local std::mt19937 gen{std::random_device{}()}; template<typename T> T random(T min, T max) { using dist = std::conditional_t< std::is_integral<T>::value, std::uniform_int_distribution<T>, std::uniform_real_distribution<T> >; return dist{min, max}(gen); } |
Coś podobnego wykorzystałem modyfikując kod z jednego ze streamów Gynvaela: [link]. W tym wypadku zysk czytelności nie był aż tak znaczący (chociaż łatwo popełnić off-by-one zastanawiając się w jakich zakresach odbywało się losowanie z rand()), ale istotny był skok jakości. Nie piszę tu nawet o równomierności rozkładu, ale o gwarancji poprawnego wykonania w wielowątkowości.
Skorzystaj z gotowych rozwiązań
Możesz skorzystać z biblioteki od twórców PCG: [opis][kod]. Przykład z dokumentacji:
#include "randutils.hpp" #include <iostream> int main() { randutils::mt19937_rng rng; std::cout << "Greetings from Office #" << rng.uniform(1,17) << " (where we think PI = " << rng.uniform(3.1,3.2) << ")\n\n" << "Our office morale is " << rng.uniform('A','D') << " grade\n" << "We " << rng.pick({"welcome", "look forward to synergizing with", "will resist", "are apathetic towards"}) << " our management overlords\n\n"; std::cout << "On the 'business intelligence' test, we scored " << rng.variate<double,std::normal_distribution>(70.0, 10.0) << "%\n"; } |
Rozwiązanie C++20 (potencjalne)
Rozważane jest dodanie szablonu funkcji std::randint. Wywołanie wyglądałoby tak:
int x = std::randint(-100,100); int y = std::randint(-100,100); |
Tutaj wszystko jest jasne (z pomocą dokumentacji) – randint otrzymuje krańce przedziałów, losowanie odbywa się w przedziałach domkniętych, więc losowanie jest w przedziale [-100, 100].
PS: żadna z tych metod nie nadaje się do kryptografii.
Kolejny dobry post, tak trzymaj 🙂
A dziękuję ;)
W jaki sposób mogłoby wyglądać zaseedowanie tego kodu? Aktualnie wyrzuca zawsze taki sam rezultat.
thread_local std::mt19937 gen{std::random_device{}()};
template
T random(T min, T max) {
return std::uniform_int_distribution{min, max}(gen);
}
Pozdrawiam
Używasz MinGW? Tam niestety random_device() zdaje się zwracać zawsze to samo. W takim przypadku dodaj tickcount i time do seed seq
gen.seed (42);
“RAND_MAX może wynosić zaledwie 32767” – może dla dzieci Windows,
dla NieWindows trochę więcej, może 2147483647?
Raczej dla kompilatorów i bibliotek standardowych zgodnych ze standardem. PS: powodzenia z większą wartością np. na prockach z 16-bitowym intem.