2016-01-22 22 views
6

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

9

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&).

+0

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

+0

Questo è bello. Ma poi devo sovraccaricare l'operatore <<() 'per ogni tipo che ha solo sovraccaricato' to_string() '. Voglio evitare un lavoro tedioso. – Lingxi

+0

@ling cosa? Perché pensi di doverlo fare? – Yakk

1

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.

+0

È 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

+0

Penso che avere il sovraccarico di '<< to_string (x)' se '<< x' non si compila sarebbe bello, se è possibile a tutti. – Lingxi

+0

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

2

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(); 
} 

LIVE

1

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; 
}