Metaprograming: il fallimento della definizione della funzione definisce una funzione separata

In questa risposta definisco un modello basato sulla proprietà is_arithmetic del tipo:

 template enable_if_t<is_arithmetic::value, string> stringify(T t){ return to_string(t); } template enable_if_t<!is_arithmetic::value, string> stringify(T t){ return static_cast(ostringstream() << t).str(); } 

dyp suggerisce che piuttosto che la proprietà is_arithmetic del tipo, che la to_string sia definita per il tipo essere i criteri di selezione del modello. Questo è chiaramente auspicabile, ma non conosco un modo per dire:

Se std::to_string non è definito, utilizzare il sovraccarico di ostringstream .

Dichiarare i criteri to_string è semplice:

 template decltype(to_string(T{})) stringify(T t){ return to_string(t); } 

È l’opposto di quei criteri che non riesco a capire come build. Questo ovviamente non funziona, ma spero che trasmetta ciò che sto cercando di build:

 template enable_if_t (T t){ return static_cast(ostringstream() << t).str(); } 

Appena votato in merito alle basi della biblioteca TS alla riunione della commissione della scorsa settimana:

 template using to_string_t = decltype(std::to_string(std::declval())); template using has_to_string = std::experimental::is_detected; 

Quindi tagga dispatch e / o SFINAE su has_to_string sul has_to_string tuo cuore.

Puoi consultare l’attuale bozza di lavoro del TS su come può essere implementato is_detected e gli amici. È piuttosto simile a can_apply nella risposta di @ Yakk.

Utilizzando il void_t Walter Brown :

 template  using void_t = void; 

È molto facile creare un tratto di questo genere:

 template struct has_to_string : std::false_type { }; template struct has_to_string()))> > : std::true_type { }; 

In primo luogo, penso che SFINAE dovrebbe di solito essere nascosto dalle interfacce. Rende l’interfaccia disordinata. Metti la SFINAE lontano dalla superficie e usa l’invio dei tag per prelevare un sovraccarico.

Secondo, nascondo persino SFINAE dalla class dei tratti. Scrivere codice “posso fare X” è abbastanza comune nella mia esperienza che non voglio dover scrivere codice SFINAE per farlo. Quindi, invece, scrivo un tratto generico can_apply e ho un tratto che SFINAE fallisce se ha passato i tipi sbagliati usando decltype .

Dopodiché, alimentiamo il carattere fallito decltype can_apply a can_apply , e otteniamo un tipo true / false a seconda che l’applicazione fallisca.

Questo riduce il lavoro per tratto “posso fare X” ad un importo minimo, e pone il codice SFINAE alquanto complicato e fragile lontano dal lavoro quotidiano.

Io uso void_t di C ++ 1z. L’implementazione da soli è semplice (in fondo a questa risposta).

Una metafunzione simile a can_apply viene proposta per la standardizzazione in C ++ 1z, ma non è così stabile come void_t , quindi non la sto usando.

Innanzitutto, uno spazio dei nomi details per hide l’implementazione di can_apply dall’essere trovata per caso:

 namespace details { templateclass Z, class, class...> struct can_apply:std::false_type{}; templateclass Z, class...Ts> struct can_apply>, Ts...>: std::true_type{}; } 

Possiamo quindi scrivere can_apply in termini di details::can_apply , e ha un’interfaccia più details::can_apply (non richiede il superamento del void extra):

 templateclass Z, class...Ts> using can_apply=details::can_apply; 

Quanto sopra è un codice metaprogramming helper generico. Una volta installato, possiamo scrivere una class di caratteri can_to_string modo molto pulito:

 template using to_string_t = decltype( std::to_string( std::declval() ) ); template using can_to_string = can_apply< to_string_t, T >; 

e abbiamo un tratto can_to_string che è vero se siamo in grado to_string una T

Il lavoro richiede di scrivere una nuova caratteristica come quella che ora è decltype 2-4 righe di codice semplice: basta creare un decltype using alias, quindi eseguire un test can_apply su di esso.

Una volta ottenuto ciò, usiamo l’invio dei tag alla corretta implementazione:

 template std::string stringify(T t, std::true_type /*can to string*/){ return std::to_string(t); } template std::string stringify(T t, std::false_type /*cannot to string*/){ return static_cast(ostringstream() << t).str(); } template std::string stringify(T t){ return stringify(t, can_to_string{}); } 

Tutto il codice brutto si nasconde nello spazio dei nomi details .

Se hai bisogno di un void_t , usa questo:

 templatestruct voider{using type=void;}; templateusing void_t=typename voider::type; 

che funziona con i principali compilatori C ++ 11.

Si noti che il templateusing void_t=void; più semplice templateusing void_t=void; non funziona su alcuni vecchi compilatori C ++ 11 (c’era un’ambiguità nello standard).

Puoi scrivere un tratto di helper per questo usando l’espressione SFINAE:

 namespace detail { //base case, to_string is invalid template  auto has_to_string_helper (...) //... to disambiguate call -> false_type; //true case, to_string valid for T template  auto has_to_string_helper (int) //int to disambiguate call -> decltype(std::to_string(std::declval()), true_type{}); } //alias to make it nice to use template  using has_to_string = decltype(detail::has_to_string_helper(0)); 

Quindi usa std::enable_if_t::value>

dimostrazione

Penso che ci siano due problemi: 1) Trova tutti gli algoritmi validi per un dato tipo. 2) Seleziona il migliore.

Ad esempio, possiamo specificare manualmente un ordine per un insieme di algoritmi sovraccaricati:

 namespace detail { template std::string stringify(choice<0>, T&& t) { using std::to_string; return to_string(std::forward(t)); } template std::string stringify(choice<1>, char const(&arr)[N]) { return std::string(arr, N); } template std::string stringify(choice<2>, T&& t) { std::ostringstream o; o << std::forward(t); return std::move(o).str(); } } 

Il primo parametro funzione specifica l’ordine tra questi algoritmi (“prima scelta”, “seconda scelta”, ..). Per selezionare un algoritmo, semplicemente spediamo alla migliore corrispondenza valida:

 template auto stringify(T&& t) -> decltype( detail::stringify(choice<0>{}, std::forward(t)) ) { return detail::stringify(choice<0>{}, std::forward(t)); } 

Come viene implementato? Abbiamo rubato un po ‘da Xeo @ Flaming Dangerzone e Paul @ void_t “può implementare i concetti”? (usando implementazioni semplificate):

 constexpr static std::size_t choice_max = 10; template struct choice : choice { static_assert(N < choice_max, ""); }; template<> struct choice {}; #include  template struct models : std::false_type {}; template struct models()...), void())> : std::true_type {}; #define REQUIRES(...) std::enable_if_t::value>* = nullptr 

Le classi di scelta ereditano da scelte peggiori: la choice<0> eredita dalla choice<1> . Pertanto, per un argomento di tipo choice<0> , un parametro di funzione di tipo choice<0> è una corrispondenza migliore rispetto alla choice<1> , che è una corrispondenza migliore della choice<2> e così via [over.ics.rank ] P4.4

Si noti che lo spareggio più specializzato si applica solo se nessuna delle due funzioni è migliore. A causa dell’ordine totale di choice s, non entreremo mai in quella situazione. Ciò impedisce alle chiamate di essere ambigue, anche se sono possibili algoritmi multipli.

Definiamo i nostri tratti del tipo:

 #include  #include  namespace helper { using std::to_string; struct has_to_string { template auto requires_(T&& t) -> decltype( to_string(std::forward(t)) ); }; struct has_output_operator { std::ostream& ostream(); template auto requires_(T&& t) -> decltype(ostream() << std::forward(t)); }; } 

I macro possono essere evitati usando un’idea di R. Martinho Fernandes :

 template using requires = std::enable_if_t::value, int>; // exemplary application: template = 0> std::string stringify(choice<0>, T&& t) { using std::to_string; return to_string(std::forward(t)); } 

Bene, puoi semplicemente saltare tutta la magia di metaprogrammazione e usare l’adattatore fit::conditional dalla libreria Fit :

 FIT_STATIC_LAMBDA_FUNCTION(stringify) = fit::conditional( [](auto x) -> decltype(to_string(x)) { return to_string(x); }, [](auto x) -> decltype(static_cast(ostringstream() << x).str()) { return static_cast(ostringstream() << x).str(); } ); 

O ancora più compatto, se non ti dispiace i macro:

 FIT_STATIC_LAMBDA_FUNCTION(stringify) = fit::conditional( [](auto x) FIT_RETURNS(to_string(x)), [](auto x) FIT_RETURNS(static_cast(ostringstream() << x).str()) ); 

Nota, ho anche limitato la seconda funzione, quindi se il tipo non può essere chiamato con to_string né trasmesso in streaming a ostringstream la funzione non può essere chiamata. Questo aiuta con messaggi di errore migliori e una migliore composibilità con il controllo dei requisiti del tipo.