2016-01-22 14 views
6

Widziałem typy, które mają odpowiadającą funkcję to_string(), ale nie przeładowały operator<<(). Tak więc, podczas wstawiania do streamowania, trzeba mieć << to_string(x), co jest pełne. Zastanawiam się, czy można napisać ogólną funkcję, która jest dostępna dla użytkowników operator<<(), a jeśli nie, cofa się do << to_string().Fallback to to_string(), gdy operator <<() zawiedzie

Odpowiedz

9

SFINAE jest przesadą, użyj ADL.

Sztuką jest, aby upewnić się, że operator<< jest dostępna, niekoniecznie jeden dostarczony przez definicji typu:

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; 

Ta sztuczka jest powszechnie stosowany w rodzajowego kodu, który musi wybierać między std::swap<T> i wyspecjalizowany Foo::swap(Foo::Bar&, Foo::Bar&).

+0

Zgadzam się, że jest to prostsze w przypadku, gdy chcesz tylko 'operator <<' dla typu 'T' zdefiniowanego w jego' przestrzeni nazw 'lub' to_string (T) 'i nic więcej, co, oczywiście, jest tym, co OP ma poprosił o, więc +1. Jeśli potrzebujesz dalszej wysyłki, to nie zadziała. Ponadto komunikaty o błędach generowane przez to rozwiązanie mogą nie być tak pomocne, jak mogłyby być. – 5gon12eder

+0

To jest miłe. Ale muszę przeciążyć 'operator <<()' dla każdego typu, który przeciął tylko 'to_string()'. Chcę uniknąć takiej żmudnej pracy. – Lingxi

+0

@ling co? Jak myślisz, dlaczego musisz to robić? – Yakk

1

Tak, jest to możliwe.

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

Ogólny operator<< będzie dostępna tylko wtedy, gdy wyrażenie to_string(obj) jest również wpisany do obj w const T& i ma wynik typu std::string. Jak już się domyśliłeś w swoim komentarzu, faktycznie jest to SFINAE w pracy. Jeśli wyrażenie decltype nie jest dobrze sformułowane, otrzymamy błąd substytucji, a przeciążenie zniknie.

Jednakże może to spowodować problemy z niejednoznacznymi przeciążeniami. Przynajmniej umieść swój kod rezerwowy operator<< w swoim własnym namespace i przeciągnij go lokalnie za pomocą deklaracji using, gdy zajdzie taka potrzeba. Myślę, że lepiej będzie napisać nazwaną funkcję, która robi to samo.

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

Następnie użyj go w taki sposób.

int 
main() 
{ 
    std::ostringstream nl {"\n"}; // has `str` member 
    out(std::cout, streamy {}, nl, stringy {}, '\n'); 
} 

Funkcja decide_how daje pełną elastyczność w podejmowaniu decyzji, jak wyjście danego rodzaju, nawet jeśli istnieje wiele dostępnych opcji. Jest również łatwy do przedłużenia. Na przykład niektóre typy mają funkcję składową str zamiast funkcji bezpłatnej ADL find-able to_string. (Właściwie to już zrobiłem.)

Funkcja detail::out używa tag dispatching, aby wybrać odpowiednią metodę wyjściową.

Predykaty can_HOW są implementowane za pomocą void_t trick, które uważam za bardzo eleganckie.

Funkcja variadic out używa modelu “for each argument” trick, który uważam za jeszcze bardziej elegancki.

Należy pamiętać, że kod to C++ 14 i będzie wymagać aktualnego kompilatora.

+0

Czy to SFINAE? Więc kiedy kompiluje 'to_string (x)', 'return os << to_string (obj);' przeciążenie istnieje, a nie inaczej? Czy mogę użyć 'std :: enable_if' zamiast' std :: conditional'? – Lingxi

+0

Myślę, że mając przeciążenie '<< to_string (x)' jeśli '<< x' nie skompilował byłoby miło, jeśli to w ogóle możliwe. – Lingxi

+0

Tak, to jest SFINAE. Zobacz zaktualizowaną odpowiedź (szczególnie w odpowiedzi na drugi komentarz). Pomyślałem o używaniu 'std :: enable_if', ale nie mogłem znaleźć prostego rozwiązania, więc poszedłem z wprawdzie nieco mylącym' std :: conditional'. – 5gon12eder

2

Spróbuj

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

Opierając się na odpowiedzi @MSalters (kredyty idą do niego), ten rozwiązuje mój problem i powinien udzielić kompletnej odpowiedzi.

#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; 
} 
Powiązane problemy