2014-12-15 11 views
15

Sto scrivendo una funzione che voglio accettare una distribuzione come parametro. Diciamo il seguente:C++, come utilizzare un sottoinsieme di tutti i tipi

#include<random> 
#include<iostream> 
using namespace std; 

random_device rd; 
mt19937 gen(rd()); 

void print_random(uniform_real_distribution<>& d) { 
    cout << d(gen); 
} 

Ora c'è un modo per generalizzare questo codice, in un breve tratto, in C++ in modo tale che essa avrebbe accettato tutte le distribuzioni e le distribuzioni solo (altrimenti il ​​compilatore dovrebbe lamentarsi)? Modifica: Per chiarire, la soluzione dovrebbe anche essere in grado di accettare solo un sottoinsieme di tutte le distribuzioni (che dovrebbe essere pre-specificato).

Accetteremo ad esempio la possibilità di definire un tipo come una raccolta di tipi consentiti ma sarebbe ancora meglio se esiste già un tipo che ha questa proprietà per le distribuzioni.

+0

Se si accettano tutte le distribuzioni, è necessario assicurarsi che il tipo sia valido per la distribuzione, altrimenti è un comportamento non definito. –

+1

@remyabel Che tipo? Pensavo che tutte le distribuzioni possano utilizzare un generatore standard come "mt19937". –

+0

Qual è la tua definizione di "solo distribuzioni". Nella mia mente tutto ciò che soddisfa l'interfaccia implicita di una distribuzione è una distribuzione. –

risposta

8

Non ci sono questi tratti nella libreria standard. Si può solo scrivere qualcosa di simile

template<typename T> 
struct is_distribution : public std::false_type {}; 

e specializzati per ogni tipo, vale a dire la distribuzione

template<typename T> 
struct is_distribution<std::uniform_int_distribution<T> > : 
public std::true_type {}; 

Poi basta

template<typename Distr> 
typename std::enable_if<is_distribution<Distr>::value>::type 
print_random(Distr& d) 
{ 
    cout << d(gen); 
} 

Inoltre, è possibile usare qualcosa come concetti-lite (ma con decltypes, dato che ora non c'è questa funzionalità), in alcuni casi non può funzionare. Nello standard ci sono delle regole, che dovrebbero seguire qualsiasi distribuzione (n3376 26.5.1.6/Tabella 118).

template<typename D> 
constexpr auto is_distribution(D& d) -> 
decltype(std::declval<typename D::result_type>(), 
std::declval<typename D::param_type>(), 
d.reset(), d.param(), d.param(std::declval<typename D::param_type>()), true); 

template<typename D> 
auto print_random(D& d) -> decltype(is_distribution(d), void()) 
{ 
} 

Se si vuole solo controllare che tipo è richiamabile con qualche generatore e l'esecuzione di questa chiamata restituisce result_type si può solo semplificare la funzione

template<typename D> 
auto is_distribution(D& d) -> 
decltype(std::is_same<typename D::result_type, 
decltype(d(*static_cast<std::mt19937*>(0)))>::value); 

tutte queste cose sarà molto semplice, quando i concetti-lite sarà disponibile in standard.

+0

eccetto che sarebbe d (gen) :-) –

+0

Ok, grazie, questo è molto utile ma piuttosto brutto :(Preferirei un modo più breve/più chiaro per ottenere questo – Haffi112

+5

@ Haffi112 Sì, il C++ in realtà non riguarda l'essere breve e chiaro –

4

Vorrei solo fare:

template<typename Distribution> 
void print_random(Distribution& d) { 
    cout << d(gen); 
} 

Tutto ciò che non soddisfa l'interfaccia implicita per una distribuzione non verrà compilato. deve avere un operator() che accetta un generatore come parametro e restituisce un valore.

+0

Questa soluzione è più concisa ma non risolve il problema di definire un tipo come un sottoinsieme di tipi consentiti – Haffi112

+1

Mi spiace, devo aver frainteso la tua domanda, hai affermato che vuoi accettare "tutte le distribuzioni". Vuoi accettare solo _some_ distribuzioni? –

+0

Il codice sopra riportato è solo un esempio di un caso d'uso E sì, se vuoi accettare solo alcune distribuzioni questa soluzione non la coprire. – Haffi112

0

In primo luogo, un po 'boilerplate per darci una prova SFINAE tipo amichevole invoke:

namespace invoke_details { 
    template<class Sig,class=void> struct invoke {}; 
    template<class F, class...Args> struct invoke< 
    F(Args...), 
    void(decltype(std::declval<F>(Args...))) 
    > { 
    using type=decltype(std::declval<F>(Args...)); 
    }; 
} 
template<class Sig> using invoke=typename invoke_details::invoke<Sig>::type; 

ora invoke< Foo(int, int) > è il tipo che si ottiene quando si prende una variabile di tipo Foo e invocare con due int s, ed è valuta in modo amichevole con SFINAE.

Questo è fondamentalmente un amichevole SFINAE std::result_of.

Successivamente, alcune cose più belle. result_type e param_type risparmiare sulle digitando altrove:

template<class T> 
using result_type = typename T::result_type; 
template<class T> 
using param_type = typename T::param_type; 

details::has_property< X, T > avrà un modello X e applicare T.Se questo succede, è true_type, altrimenti false_type:

namespace details { 
    template<template<class>class X, class T, class=void> 
    struct has_property : std::false_type {}; 
    template<template<class>class X, class T> 
    struct has_property<X,T,void(X<T>)> : std::true_type {}; 
} 

Questo ci dà has_result_type ecc in un modo piuttosto:

template<class T> 
using has_result_type = details::has_property< result_type, T >; 
template<class T> 
using has_param_type = details::has_property< param_type, T >; 
template<class Sig> 
using can_invoke = details::has_property< invoke, Sig >; 
template<class T> 
using can_twist_invoke = can_invoke< T(std::mt19937) >; 

Credo che la semplicità di queste dichiarazioni vale la boilerplate in precedenza.

Ora, un po 'di metaprogrammazione booleano:

template<bool...> struct all_of : std::true_type {}; 
template<bool b0, bool... bs> struct all_of : std::integral_constant< bool, b0 && all_of<bs...>{} > {}; 

template<class T, template<class>class... Tests> 
using passes_tests = all_of< Tests<T>{}... >; 

e otteniamo il nostro una linea abbastanza is_distribution:

template<class T> 
using is_distribution = passes_tests< T, has_result_type, has_param_type, can_twist_invoke >; 

Questo non copre ancora .param o .reset.

Questo stile porta a più codice, ma la "cattiva" roba viene nascosta nei dettagli degli spazi dei nomi. Qualcuno che vede is_distribution può guardare la definizione e vedere cosa si intende per autodocumentazione. Solo dopo aver eseguito il drill down vedranno i dettagli di implementazione più confusi.