Ho visto tipi che hanno la corrispondente funzione to_string()
, ma non hanno sovraccaricato lo operator<<()
. Quindi, quando si inserisce lo streaming, si deve << to_string(x)
che è dettagliato. Mi chiedo se sia possibile scrivere una funzione generica per gli utenti operator<<()
se supportata e torna a << to_string()
in caso contrario.Fallback to to_string() quando l'operatore <<() non riesce
risposta
SFINAE è eccessivo, utilizzare ADL.
Il trucco è quello di assicurarsi che unoperator<<
è disponibile, non necessariamente quello fornito dalla definizione del tipo:
namespace helper {
template<typename T> std::ostream& operator<<(std::ostream& os, T const& t)
{
return os << to_string(t);
}
}
using helper::operator<<;
std::cout << myFoo;
Questo trucco è comunemente usato in codice generico che ha bisogno di scegliere tra std::swap<T>
e uno specialista Foo::swap(Foo::Bar&, Foo::Bar&)
.
Sì, è possibile.
#include <iostream>
#include <sstream>
#include <string>
#include <type_traits>
struct streamy
{
};
std::ostream&
operator<<(std::ostream& os, const streamy& obj)
{
return os << "streamy [" << static_cast<const void *>(&obj) << "]";
}
struct stringy
{
};
std::string
to_string(const stringy& obj)
{
auto oss = std::ostringstream {};
oss << "stringy [" << static_cast<const void *>(&obj) << "]";
return oss.str();
}
template <typename T>
std::enable_if_t
<
std::is_same
<
std::string,
decltype(to_string(std::declval<const T&>()))
>::value,
std::ostream
>&
operator<<(std::ostream& os, const T& obj)
{
return os << to_string(obj);
}
int
main()
{
std::cout << streamy {} << '\n';
std::cout << stringy {} << '\n';
}
Il generico operator<<
saranno disponibili solo se l'espressione to_string(obj)
è ben digitato-per obj
un const T&
e ha un risultato di tipo std::string
. Come hai già congetturato nel tuo commento, questa è davvero SFINAE al lavoro. Se l'espressione decltype
non è ben formata, avremo un errore di sostituzione e il sovraccarico scomparirà.
Tuttavia, questo probabilmente ti porterà in difficoltà con sovraccarichi ambigui. Per lo meno, inserisci il fallback operator<<
nel suo namespace
e trascinalo in locale solo tramite una dichiarazione using
quando necessario. Penso che starai meglio scrivendo una funzione con nome che fa la stessa cosa.
namespace detail
{
enum class out_methods { directly, to_string, member_str, not_at_all };
template <out_methods> struct tag {};
template <typename T>
void
out(std::ostream& os, const T& arg, const tag<out_methods::directly>)
{
os << arg;
}
template <typename T>
void
out(std::ostream& os, const T& arg, const tag<out_methods::to_string>)
{
os << to_string(arg);
}
template <typename T>
void
out(std::ostream& os, const T& arg, const tag<out_methods::member_str>)
{
os << arg.str();
}
template <typename T>
void
out(std::ostream&, const T&, const tag<out_methods::not_at_all>)
{
// This function will never be called but we provide it anyway such that
// we get better error messages.
throw std::logic_error {};
}
template <typename T, typename = void>
struct can_directly : std::false_type {};
template <typename T>
struct can_directly
<
T,
decltype((void) (std::declval<std::ostream&>() << std::declval<const T&>()))
> : std::true_type {};
template <typename T, typename = void>
struct can_to_string : std::false_type {};
template <typename T>
struct can_to_string
<
T,
decltype((void) (std::declval<std::ostream&>() << to_string(std::declval<const T&>())))
> : std::true_type {};
template <typename T, typename = void>
struct can_member_str : std::false_type {};
template <typename T>
struct can_member_str
<
T,
decltype((void) (std::declval<std::ostream&>() << std::declval<const T&>().str()))
> : std::true_type {};
template <typename T>
constexpr out_methods
decide_how() noexcept
{
if (can_directly<T>::value)
return out_methods::directly;
else if (can_to_string<T>::value)
return out_methods::to_string;
else if (can_member_str<T>::value)
return out_methods::member_str;
else
return out_methods::not_at_all;
}
template <typename T>
void
out(std::ostream& os, const T& arg)
{
constexpr auto how = decide_how<T>();
static_assert(how != out_methods::not_at_all, "cannot format type");
out(os, arg, tag<how> {});
}
}
template <typename... Ts>
void
out(std::ostream& os, const Ts&... args)
{
const int dummy[] = {0, ((void) detail::out(os, args), 0)...};
(void) dummy;
}
Quindi utilizzarlo in questo modo.
int
main()
{
std::ostringstream nl {"\n"}; // has `str` member
out(std::cout, streamy {}, nl, stringy {}, '\n');
}
La funzione decide_how
ti dà la massima flessibilità nel decidere come all'uscita di un dato tipo, anche se ci sono diverse opzioni disponibili. È anche facile da estendere. Ad esempio, alcuni tipi hanno una funzione membro str
invece di una funzione libera ADL reperibile to_string
. (In realtà, l'ho già fatto.)
La funzione detail::out
utilizza tag dispatching per selezionare il metodo di uscita appropriato.
I predicati can_HOW
vengono implementati utilizzando lo void_t
trick che trovo molto elegante.
La variadica funzione out
utilizza lo “for each argument” trick, che trovo ancora più elegante.
Si noti che il codice è C++ 14 e richiederà un compilatore aggiornato.
È questo SFINAE? Quindi, quando 'to_string (x)' compila, il 'return os << to_string (obj);' esiste il sovraccarico e non altrimenti? Potrei usare 'std :: enable_if' invece di' std :: condizionale'? – Lingxi
Penso che avere il sovraccarico di '<< to_string (x)' se '<< x' non si compila sarebbe bello, se è possibile a tutti. – Lingxi
Sì, questo è SFINAE. Si prega di consultare la risposta aggiornata (soprattutto in risposta al secondo commento). Ho pensato di usare 'std :: enable_if' ma non sono riuscito a trovare una soluzione straight-forward, quindi ho optato per lo std :: conditional, un po 'confuso. – 5gon12eder
Prova
template <typename T>
void print_out(T t) {
print_out_impl(std::cout, t, 0);
}
template <typename OS, typename T>
void print_out_impl(OS& o, T t,
typename std::decay<decltype(
std::declval<OS&>() << std::declval<T>()
)>::type*) {
o << t;
}
template <typename OS, typename T>
void print_out_impl(OS& o, T t, ...) {
o << t.to_string();
}
Sulla base della risposta di @MSalters (crediti va a lui), questo risolve il mio problema e dovrebbe dare una risposta completa.
#include <iostream>
#include <string>
#include <type_traits>
struct foo_t {};
std::string to_string(foo_t) {
return "foo_t";
}
template <class CharT, class Traits, class T>
typename std::enable_if<std::is_same<CharT, char>::value,
std::basic_ostream<CharT, Traits>&>::type
operator<<(std::basic_ostream<CharT, Traits>& os, const T& x) {
return os << to_string(x);
}
int main() {
std::cout << std::string{"123"} << std::endl;
std::cout << foo_t{} << std::endl;
}
Sono d'accordo che questo è più semplice nel caso in cui si desidera solo l'operatore << 'per il tipo' T' definito nel suo 'namespace' o' to_string (T) 'e niente di più, che, certamente, è ciò che l'OP ha chiesto, quindi +1. Se è necessario spedire ulteriormente, questo non funzionerà. Inoltre, i messaggi di errore generati da questa soluzione potrebbero non essere così utili come potrebbero essere. – 5gon12eder
Questo è bello. Ma poi devo sovraccaricare l'operatore <<() 'per ogni tipo che ha solo sovraccaricato' to_string() '. Voglio evitare un lavoro tedioso. – Lingxi
@ling cosa? Perché pensi di doverlo fare? – Yakk