Nie używaj rand(), C++ ma <random>

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.

7 thoughts on “Nie używaj rand(), C++ ma <random>

  1. 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

    1. Używasz MinGW? Tam niestety random_device() zdaje się zwracać zawsze to samo. W takim przypadku dodaj tickcount i time do seed seq

  2. “RAND_MAX może wynosić zaledwie 32767” – może dla dzieci Windows,
    dla NieWindows trochę więcej, może 2147483647?

    1. Raczej dla kompilatorów i bibliotek standardowych zgodnych ze standardem. PS: powodzenia z większą wartością np. na prockach z 16-bitowym intem.

Leave a Reply

Your email address will not be published.