Este código de exemplo ilustra que std::rand
é um caso de balderdash de culto de carga legado que deve fazer você levantar as sobrancelhas toda vez que você vê-lo.
Existem várias questões aqui:
O pessoal do contrato geralmente assume - mesmo as pobres almas infelizes que não conhecem nada melhor e não pensam nisso precisamente nestes termos - é que as rand
amostras da distribuição uniforme nos inteiros em 0, 1, 2, ... RAND_MAX
,, e cada chamada produz uma amostra independente .
O primeiro problema é que o contrato assumido, amostras aleatórias uniformes independentes em cada chamada, não é realmente o que diz a documentação - e, na prática, as implementações historicamente falharam em fornecer nem mesmo o mais básico simulacro de independência. Por exemplo, C99 §7.20.2.1 'A rand
função' diz, sem elaboração:
A rand
função calcula uma sequência de inteiros pseudoaleatórios no intervalo de 0 a RAND_MAX
.
Esta é uma frase sem sentido, porque a pseudo-aleatoriedade é uma propriedade de uma função (ou família de funções ), não de um número inteiro, mas isso não impede nem mesmo os burocratas da ISO de abusar da linguagem. Afinal, os únicos leitores que ficariam chateados sabem que não é melhor ler a documentação rand
por medo de que suas células cerebrais se deteriorem.
Uma implementação histórica típica em C funciona assim:
static unsigned int seed = 1;
static void
srand(unsigned int s)
{
seed = s;
}
static unsigned int
rand(void)
{
seed = (seed*1103515245 + 12345) % ((unsigned long)RAND_MAX + 1);
return (int)seed;
}
Isso tem a propriedade infeliz de que , embora uma única amostra possa ser uniformemente distribuída sob uma semente aleatória uniforme (que depende do valor específico de RAND_MAX
), ela alterna entre inteiros pares e ímpares em chamadas consecutivas - após
int a = rand();
int b = rand();
a expressão (a & 1) ^ (b & 1)
produz 1 com 100% de probabilidade, o que não é o caso para amostras aleatórias independentes em qualquer distribuição suportada em inteiros pares e ímpares. Assim, surgiu um culto à carga em que se deve descartar os bits de ordem inferior para perseguir a besta indescritível de 'melhor aleatoriedade'. (Alerta de spoiler: este não é um termo técnico. Este é um sinal de que qualquer prosa que você está lendo não sabe do que está falando ou pensa que você é um ignorante e deve ser condescendente.)
O segundo problema é que mesmo se cada chamada fosse amostrada independentemente de uma distribuição aleatória uniforme em 0, 1, 2, ... RAND_MAX
, o resultado de rand() % 6
não seria uniformemente distribuído em 0, 1, 2, 3, 4, 5 como um dado role, a menos que RAND_MAX
seja congruente com -1 módulo 6. Contra-exemplo simples: Se RAND_MAX
= 6, então de rand()
, todos os resultados têm probabilidade igual 1/7, mas de rand() % 6
, o resultado 0 tem probabilidade 2/7, enquanto todos os outros resultados têm probabilidade 1/7 .
A maneira certa de fazer isso é com a amostragem de rejeição: retire repetidamente uma amostra aleatória uniforme independente s
de 0, 1, 2, ... RAND_MAX
, e rejeite (por exemplo) os resultados 0, 1, 2, ..., ((RAND_MAX + 1) % 6) - 1
- se você obtiver um dos aqueles, recomece; caso contrário, rendimento s % 6
.
unsigned int s;
while ((s = rand()) < ((unsigned long)RAND_MAX + 1) % 6)
continue;
return s % 6;
Dessa forma, o conjunto de resultados rand()
que aceitamos é igualmente divisível por 6, e cada resultado possível de s % 6
é obtido pelo mesmo número de resultados aceitos de rand()
, portanto, se rand()
for uniformemente distribuído, então o é s
. Não há limite para o número de tentativas, mas o número esperado é menor que 2, e a probabilidade de sucesso aumenta exponencialmente com o número de tentativas.
A escolha de quais resultados rand()
você rejeita é imaterial, desde que você mapeie um número igual deles para cada número inteiro abaixo de 6. O código em cppreference.com faz uma escolha diferente , por causa do primeiro problema acima - que nada é garantido sobre o distribuição ou independência de saídas de rand()
, e na prática, os bits de ordem inferior exibiram padrões que não 'parecem aleatórios o suficiente' (não importa que a próxima saída seja uma função determinística da anterior).
Exercício para o leitor: Prove que o código em cppreference.com produz uma distribuição uniforme nas jogadas de dados se rand()
produz uma distribuição uniforme em 0, 1, 2,… RAND_MAX
,.
Exercício para o leitor: Por que você prefere rejeitar um ou outro subconjunto? Qual cálculo é necessário para cada tentativa nos dois casos?
Um terceiro problema é que o espaço da semente é tão pequeno que, mesmo que a semente seja distribuída uniformemente, um adversário armado com conhecimento do seu programa e um resultado, mas não a semente, pode prever prontamente a semente e os resultados subsequentes, o que os faz parecer que não afinal aleatório. Portanto, nem pense em usar isso para criptografia.
Você pode seguir o caminho sofisticado da superengenharia e as std::uniform_int_distribution
aulas de C ++ 11 com um dispositivo aleatório apropriado e seu mecanismo aleatório favorito como o sempre popular tornado de Mersenne std::mt19937
para jogar dados com seu primo de quatro anos, mas mesmo isso não vai estar apto para gerar material de chave criptográfica - e o Mersenne twister é um terrível devorador de espaço também com um estado de vários kilobytes causando estragos no cache de sua CPU com um tempo de configuração obsceno, por isso é ruim mesmo para, por exemplo , simulações de Monte Carlo paralelas com árvores reproduzíveis de subcomputações; sua popularidade provavelmente decorre principalmente de seu nome atraente. Mas você pode usá-lo para rolar dados de brinquedo como este exemplo!
Outra abordagem é usar um gerador de números pseudo-aleatórios criptográficos simples com um estado pequeno, como um PRNG de apagamento rápido de chave simples ou apenas uma cifra de fluxo, como AES-CTR ou ChaCha20, se você estiver confiante ( por exemplo , em uma simulação de Monte Carlo para pesquisa em ciências naturais) de que não há consequências adversas em prever resultados passados se o estado for comprometido.
std::uniform_int_distribution
para dados