Números aleatorios en C++: random, mt19937, distribuciones y secretos

Números aleatorios en C++ con la biblioteca <random>: un generador std::mt19937 y una distribución para cada caso, semillas fijas para repetir resultados y distintas para que no se repitan, barajar y escoger al azar, por qué rand() % n tiene sesgo y qué usar cuando el número es un secreto.

Un dado con mt19937 y uniform_int_distribution

La biblioteca <random> separa dos piezas. El generador produce números enteros sin forma, y la distribución los convierte en lo que necesitas: un entero entre 1 y 6, un decimal entre 0 y 1, una campana de Gauss… El generador más usado es std::mt19937, el Mersenne Twister:

#include <print>
#include <random>

int main()
{
    std::mt19937 gen(42);                               // semilla fija: siempre la misma secuencia
    std::uniform_int_distribution<int> dado(1, 6);      // enteros de 1 a 6, ambos incluidos

    std::print("10 tiradas:");
    for (int i = 0; i < 10; ++i) std::print(" {}", dado(gen));
    std::println("");

    int veces[7] = {};
    for (int i = 0; i < 60'000; ++i) ++veces[dado(gen)];
    for (int cara = 1; cara <= 6; ++cara) std::println("cara {}: {} veces", cara, veces[cara]);
}
salida10 tiradas: 3 5 6 2 5 5 4 4 1 3
cara 1: 9890 veces
cara 2: 10038 veces
cara 3: 9990 veces
cara 4: 10078 veces
cara 5: 10093 veces
cara 6: 9911 veces

Con 60,000 tiradas, cada cara sale cerca de 10,000 veces, como debe ser. Como la semilla es fija, el programa da siempre las mismas tiradas: es lo que se quiere en pruebas, simulaciones que hay que repetir o juegos con partidas que se pueden compartir.

Mismo generador, distinta salida. El estándar fija exactamente qué números da std::mt19937 con cada semilla, pero no cómo los transforman las distribuciones. Con GCC (libstdc++), Clang en macOS (libc++) y Visual Studio, el mismo programa puede dar tiradas distintas. Si necesitas resultados idénticos en todas las plataformas, escribe tú la transformación o usa una biblioteca que la fije.

Comprobar el generador

Precisamente porque std::mt19937 está fijado por el estándar, se puede comprobar: con la semilla por omisión, el número 10,000 que produce tiene que ser 4,123,659,995 en cualquier compilador.

#include <print>
#include <random>

int main()
{
    std::mt19937 gen;                 // semilla por omisión: 5489
    gen.discard(9'999);               // salta los 9,999 primeros
    std::println("el número 10,000 es {}", gen());
    std::println("mínimo y máximo del generador: {} y {}", gen.min(), gen.max());
}
salidael número 10,000 es 4123659995
mínimo y máximo del generador: 0 y 4294967295

Otras distribuciones

#include <cmath>
#include <map>
#include <print>
#include <random>
#include <string>

int main()
{
    std::mt19937 gen(7);

    std::uniform_real_distribution<double> real(0.0, 1.0);     // decimal en [0, 1)
    std::println("decimal: {:.4f}", real(gen));

    std::normal_distribution<double> altura(170.0, 8.0);         // media 170 cm, desviación 8
    std::print("alturas:");
    for (int i = 0; i < 5; ++i) std::print(" {:.1f}", altura(gen));
    std::println("");

    std::bernoulli_distribution moneda(0.5);                     // true o false
    std::println("moneda: {}", moneda(gen) ? "cara" : "cruz");

    // Con pesos: el premio 0 sale el 70 % de las veces, el 1 el 25 % y el 2 el 5 %
    std::discrete_distribution<int> premio({70, 25, 5});
    const std::string nombres[] = {"nada", "camiseta", "consola"};
    std::map<std::string, int> veces;
    for (int i = 0; i < 10'000; ++i) ++veces[nombres[premio(gen)]];
    for (const auto& [p, n] : veces) std::println("{:<9} {:5.1f} %", p, n / 100.0);
}
salidadecimal: 0.2273
alturas: 155.0 166.5 166.9 168.2 167.8
moneda: cruz
camiseta   25.7 %
consola     4.8 %
nada       69.5 %

Mezclar y escoger al azar

Para barajar un vector se usa std::shuffle con un generador, y para escoger varios elementos sin repetir, std::sample. std::random_shuffle, que usaba rand() por dentro, se eliminó en C++17.

#include <algorithm>
#include <iterator>
#include <print>
#include <random>
#include <string>
#include <vector>

int main()
{
    std::mt19937 gen(2026);
    std::vector<int> cartas{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    std::ranges::shuffle(cartas, gen);
    std::println("baraja: {}", cartas);

    std::vector<std::string> equipo{"Ana", "Bruno", "Carla", "David", "Elena", "Fran"};
    std::vector<std::string> elegidos;
    std::ranges::sample(equipo, std::back_inserter(elegidos), 3, gen);
    std::println("elegidos para la guardia: {}", elegidos);

    std::uniform_int_distribution<std::size_t> indice(0, equipo.size() - 1);
    std::println("hoy invita: {}", equipo[indice(gen)]);
}
salidabaraja: [6, 4, 5, 1, 3, 8, 7, 10, 9, 2]
elegidos para la guardia: ["Carla", "David", "Fran"]
hoy invita: Carla

std::sample respeta el orden original de los elegidos. Si los quieres en orden aleatorio, baraja el resultado después.

Por qué no usar rand() % n

El rand() heredado de C tiene tres problemas: su calidad depende de la plataforma, su rango puede ser muy corto (RAND_MAX es solo 32,767 en Visual Studio) y usa un estado global, compartido por todo el programa e inseguro con hilos. Además, la forma típica de usarlo, rand() % 6 + 1, tiene sesgo: si el número de valores posibles no es múltiplo de 6, unas caras salen más que otras. Se ve muy claro con un generador pequeño de 256 valores:

#include <print>

int main()
{
    // Supongamos un generador perfecto que da los 256 valores de 0 a 255 con la misma probabilidad
    int veces[6] = {};
    for (int x = 0; x < 256; ++x) ++veces[x % 6];

    for (int cara = 0; cara < 6; ++cara)
        std::println("cara {}: {} de 256 ({:.2f} %)", cara + 1, veces[cara], veces[cara] * 100.0 / 256);
}
salidacara 1: 43 de 256 (16.80 %)
cara 2: 43 de 256 (16.80 %)
cara 3: 43 de 256 (16.80 %)
cara 4: 43 de 256 (16.80 %)
cara 5: 42 de 256 (16.41 %)
cara 6: 42 de 256 (16.41 %)

256 no es múltiplo de 6, así que las caras 1 a 4 salen 43 veces y las 5 y 6 solo 42. Con un rango grande el sesgo es más pequeño, pero existe. std::uniform_int_distribution lo evita descartando los valores sobrantes, y por eso es la forma correcta de sacar un número en un rango.

Semilla distinta en cada ejecución

Para que un juego no repita siempre la misma partida, la semilla debe cambiar. La forma habitual es pedírsela a std::random_device, que en Linux, macOS y Windows con compiladores actuales obtiene números del sistema operativo:

#include <print>
#include <random>

int main()
{
    std::random_device rd;                // números del sistema operativo
    std::mt19937 gen(rd());               // semilla distinta en cada ejecución
    std::uniform_int_distribution<int> dado(1, 6);
    std::println("tirada: {}", dado(gen));
}

Este programa da un resultado distinto cada vez, por eso aquí no copiamos su salida. Con una sola llamada a rd() la semilla tiene 32 bits, suficiente para juegos y simulaciones. No conviene sembrar con la hora (time(nullptr)): dos programas lanzados en el mismo segundo tendrían la misma secuencia.

Números para secretos: tokens, contraseñas y claves

Nada de lo anterior sirve para generar un secreto. std::mt19937 es predecible: es un algoritmo público, y quien vea suficientes números seguidos puede calcular su estado interno y adivinar todos los siguientes. Para un token de sesión, una contraseña temporal o una clave hace falta un generador criptográficamente seguro, y en C++ estándar no hay ninguno garantizado: std::random_device suele usar el generador del sistema, pero el estándar permite que no lo haga, y ha habido plataformas en las que daba siempre la misma secuencia.

Lo seguro es pedir los bytes directamente al sistema operativo. En Linux, con getrandom:

#include <array>
#include <cstddef>
#include <format>
#include <print>
#include <stdexcept>
#include <string>
#include <sys/random.h>

std::string token_hex(std::size_t bytes)
{
    std::array<unsigned char, 64> buf{};
    if (bytes > buf.size()) throw std::invalid_argument("token demasiado largo");

    std::size_t leidos = 0;
    while (leidos < bytes) {                          // getrandom puede devolver menos bytes de los pedidos
        ssize_t n = getrandom(buf.data() + leidos, bytes - leidos, 0);
        if (n < 0) throw std::runtime_error("getrandom falló");
        leidos += static_cast<std::size_t>(n);
    }
    std::string hex;
    for (std::size_t i = 0; i < bytes; ++i) hex += std::format("{:02x}", buf[i]);
    return hex;
}

int main()
{
    std::string t = token_hex(16);                    // 128 bits
    // El token cambia en cada ejecución: aquí solo comprobamos su forma
    bool solo_hex = t.find_first_not_of("0123456789abcdef") == std::string::npos;
    std::println("token de {} caracteres, solo hexadecimales: {}", t.size(), solo_hex);
}
salidatoken de 32 caracteres, solo hexadecimales: true

En macOS y los BSD el equivalente es arc4random_buf, y en Windows, BCryptGenRandom. Si el programa ya usa OpenSSL o libsodium, sus funciones RAND_bytes y randombytes_buf son igual de válidas. Y si el token se compara con uno guardado, la comparación debe tardar lo mismo acierte o falle, para no dar pistas: más en Los fallos de seguridad típicos de C++.

  • ParaUsaNo uses
  • Juegos, simulaciones, pruebasstd::mt19937 + una distribuciónrand() % n
  • Resultados repetiblesstd::mt19937 con semilla fijastd::random_device
  • Semilla distinta en cada ejecuciónstd::random_devicetime(nullptr)
  • Barajarstd::ranges::shufflestd::random_shuffle (eliminado en C++17)
  • Tokens, contraseñas, clavesgetrandom, arc4random_buf, BCryptGenRandom o OpenSSLstd::mt19937 y cualquier generador de <random>

Todo el código de esta página se compiló con GCC 15.3 (C++23, -std=c++23 -Wall -Wextra, sin avisos) y se ejecutó en Linux, en un contenedor limpio sin acceso a la red, antes de publicarla; las salidas están copiadas de esa ejecución.