2014-07-04 5 views
15

Sto appena iniziando a usare l'intestazione <random> di C++ 11 per la prima volta, ma ci sono ancora alcune cose che sembrano un po 'misteriose. Questa domanda riguarda il modo inteso, idiomatico e best-practice per svolgere un compito molto semplice.Utilizzando l'intestazione <random> di C++ 11, qual è il modo corretto per ottenere un numero intero compreso tra 0 e n?

Attualmente, in una parte del mio codice ho qualcosa di simile:

std::default_random_engine eng {std::random_device{}()}; 
std::uniform_int_distribution<> random_up_to_A {0, A}; 
std::uniform_int_distribution<> random_up_to_B {0, B}; 
std::uniform_int_distribution<> random_up_to_some_other_constant {0, some_other_constant}; 

e poi quando voglio un intero compreso tra 0 e B chiamo random_up_to_B(eng).

Poiché questo sta iniziando a sembrare un po 'sciocco, voglio implementare una funzione rnd tale che rnd(n, eng) restituisca un numero casuale compreso tra 0 e n.

qualcosa come il seguente dovrebbe funzionare

template <class URNG> 
int rnd(int n, URNG &eng) { 
    std::uniform_int_distribution<> dist {0, n}; 
    return dist(eng); 
} 

ma che comporta la creazione di un nuovo oggetto di distribuzione ogni volta, e ho l'impressione che non è il modo in cui si suppone di farlo.

Quindi la mia domanda è: qual è il modo migliore e più pratico per eseguire questo semplice compito, utilizzando le astrazioni fornite dall'intestazione <random>? Lo chiedo perché sono costretto a voler fare cose molto più complicate di questo in seguito, e voglio assicurarmi che sto usando questo sistema nel modo giusto.

+0

Hai qualche indicazione che la creazione di un 'uniform_int_distribution' è costosa? Sospetto che non lo sia.Potresti trovare delle indicazioni nella documentazione di 'boost' che era la genesi di' random'. –

+0

@MarkRansom Immagino che la creazione di un nuovo 'uniform_int_distribution' possa essere inline e non abbia alcun costo. Ma per altre distribuzioni questo potrebbe non essere il caso, dal momento che l'implementazione potrebbe dover memorizzare lo stato. La questione non è tanto l'implementazione di questa specifica cosa, quanto la comprensione di come '' sia destinato ad essere usato in generale. – Nathaniel

+1

Le distribuzioni sono progettate per essere economiche da costruire. È il motore casuale che è costoso. –

risposta

7

uniform_int_distribution non dovrebbe essere costoso da costruire, quindi crearne uno ogni volta con nuovi limiti dovrebbe essere OK. Tuttavia, c'è un modo per usare lo stesso oggetto con nuovi limiti, ma è ingombrante.

uniform_int_distribution::operator() ha un sovraccarico che accetta un oggetto uniform_int_distribution::param_type che può indicare i nuovi limiti da utilizzare, ma param_type sé un tipo opaco, e non c'è modo di costruire portatile tranne estraendolo da un uniform_int_distribution un'istanza esistente. Ad esempio, la seguente funzione può essere utilizzata per creare un uniform_int_distribution::param_type.

std::uniform_int_distribution<>::param_type 
    make_param_type(int min, int max) 
{ 
    return std::uniform_int_distribution<>(min, max).param(); 
} 

Passare a operator() e il risultato generato sarà nell'intervallo specificato.

Live demo

Quindi, se si vuole veramente per riutilizzare lo stesso uniform_int_distribution, creare e salvare più istanze del param_type utilizzando la funzione di cui sopra, e utilizzare questi quando si chiama operator().


La risposta sopra è impreciso, in quanto lo standard non specifica che il param_type può essere costruito dagli stessi argomenti distribuzione come quelle utilizzate dal costruttore del tipo di distribuzione corrispondente. Grazie a @ T.C. per pointing this out.

Da §26.5.1.6/9 [rand.req.dist]

Per ciascuno dei costruttori di D prendono argomenti corrispondenti ai parametri della distribuzione, P deve avere un costruttore corrispondente soggetto agli stessi requisiti e tenendo argomenti identici a valori numero, tipo e predefiniti. ...

Quindi non abbiamo bisogno per costruire l'oggetto di distribuzione inutilmente solo per estrarre il param_type. Invece la funzione make_param_type può essere modificato per

template <typename Distribution, typename... Args> 
typename Distribution::param_type make_param_type(Args&&... args) 
{ 
    return typename Distribution::param_type(std::forward<Args>(args)...); 
} 

che può essere utilizzato come

make_param_type<std::uniform_int_distribution<>>(0, 10) 

Live demo

+0

Sembra che tu possa impostare i parametri dell'oggetto' param_type' senza ottenerli da una distribuzione esistente. Potresti guardare la mia risposta e vedere se c'è qualcosa di sbagliato in questo? (In particolare hai menzionato la portabilità, e non so se sto facendo qualcosa che potrebbe rovinare tutto.) – Nathaniel

7

Rispondendo alla mia domanda: adattando un esempio trovato nella this document, il seguente sembra essere il modo corretto per implementare una funzione restituendo un numero intero casuale compreso tra 0 e n-1:

template<class URNG> 
int rnd(int n, URNG &engine) { 
    using dist_t = std::uniform_int_distribution<>; 
    using param_t = dist_t::param_type; 

    static dist_t dist; 
    param_t params{0,n-1}; 

    return dist(engine, params); 
} 

Per renderlo thread-safe si deve evitare la dichiarazione static. Una possibilità è quella di fare una classe convenienza in questo senso, che è quello che sto usando nel mio proprio codice:

template<class URNG> 
class Random { 
public:   
    Random(): engine(std::random_device{}()) {} 
    Random(typename std::result_of<URNG()>::type seed): engine(seed) {} 

    int integer(int n) { 
     std::uniform_int_distribution<>::param_type params {0, n-1}; 
     return int_dist(engine, params); 
    } 

private: 
    URNG engine; 
    std::uniform_int_distribution<> int_dist; 
}; 

Questo viene creata un'istanza con (per esempio) Random<std::default_random_engine> rnd, e gli interi casuali possono essere ottenuti con rnd.integer(n). I metodi per il campionamento da altre distribuzioni possono essere facilmente aggiunti a questa classe.

Per ripetere ciò che ho detto nei commenti, riutilizzare l'oggetto di distribuzione non è probabilmente necessario per lo specifico compito di campionare gli interi in modo uniforme, ma per altre distribuzioni penso che sarà più efficiente rispetto a crearlo ogni volta, perché ci sono alcuni algoritmi per il campionamento da alcune distribuzioni che possono salvare cicli della CPU generando più valori contemporaneamente. (In linea di principio anche uniform_int_distribution potrebbe farlo, tramite la vettorizzazione SIMD.) Se non è possibile aumentare l'efficienza mantenendo l'oggetto di distribuzione, è difficile immaginare perché avrebbero progettato l'API in questo modo.

Urrà per C++ e la sua complessità inutile! Questo conclude un lavoro pomeridiano che compie un semplice compito di cinque minuti, ma almeno ho un'idea molto migliore di quello che sto facendo ora.

+2

Ho pensato di postare anche questo, ma ho deciso contro perché non è portabile. Non c'è bisogno di 'param_type' per avere un costruttore di argomenti a 2 accessibili, anche se ciò che hai fatto funziona su gcc, clang e VS2013. – Praetorian

+0

La tua soluzione ideale è scrivere questo codice per qualsiasi implementazione tu sappia che funzioni, e per altre implementazioni, fai quello che @Praetorian menziona. – Mehrdad

+0

È interessante notare che la proposta a cui ti sei collegato dichiara che questo è il modo in cui costruisci il 'param_type', e che è un tipo annidato. Tuttavia, lo standard non dice nulla sulla sua costruzione e afferma esplicitamente che non è specificato se si tratta di un tipo annidato o di un typedef. Mi chiedo cosa abbia causato i cambiamenti tra la proposta e l'inclusione nello standard. – Praetorian

2

Il modo gergale per generare il codice in base a parametri variabili è quello di creare oggetti di distribuzione, se necessario, a Vary range of uniform_int_distribution:

std::random_device rd; 
std::default_random_engine eng{rd()}; 
int n = std::uniform_int_distribution<>{0, A}(eng); 

Se si teme che le prestazioni possono essere ostacolato da non riuscire a sfruttare appieno stato interno della distribuzione , è possibile utilizzare una singola distribuzione e passare diversi parametri di volta in volta:

std::random_device rd; 
std::default_random_engine eng{rd()}; 
std::uniform_int_distribution<> dist; 
int n = dist(eng, decltype(dist)::param_type{0, A}); 

Se questo sembra complicato, si consideri che per la maggior parte scopi si generare numeri casuali secondo la stessa distribuzione con gli stessi parametri (da qui il costruttore della distribuzione che assume i parametri); variando i parametri si sta già entrando in territorio avanzato.