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 diostringstream
.
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 template
più semplice template
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
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.