Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I detect if a type can be streamed to an std::ostream?

I'm trying to write a type trait to detect if a type has overloaded operator<<() suitable to use to an output stream.

I'm missing something because I'm always getting true for a simple empty class with no operators at all.

Here the code:

template<typename S, typename T>
class is_streamable
{
    template<typename SS, typename TT>
    static auto test(SS&& s, TT&& t)
    -> decltype(std::forward<SS>(s) << std::forward<TT>(t));

    struct dummy_t {};
    static dummy_t test(...);

    using return_type = decltype(test(std::declval<S>(), std::declval<T>()));

public:
    static const bool value = !std::is_same<return_type, dummy_t>::value;
};

class C {};

int main() {
    std::cout << is_streamable<std::stringstream, C>::value << std::endl;
    return 0;
}

Output:

1

Here it is in ideone: https://ideone.com/ikSBoT

What am I doing wrong?

like image 642
gigabytes Avatar asked Mar 31 '14 09:03

gigabytes


2 Answers

It's apparently this overload of operator<< that's stepping in your way and making the expression in traling return type valid:

template< class CharT, class Traits, class T >
basic_ostream< CharT, Traits >& operator<<( basic_ostream<CharT,Traits>&& os,
                                            const T& value );

See (3) on this reference page. It's a simple forwarder (calling os << value) that was added in C++11 to allow insertion to rvalue-streams because they don't bind to overloads taking an lvalue reference.

So, the problem is that std::declval<SS>() returns an rvalue reference and this overload kicks in. The call itself is well-formed, but because the function itself does not get instantiated you don't get an error even if value is not streamable.

This can be sidestepped if you explicitly ask for lvalue reference: std::declval<SS&>().

I'd also suggest a slightly different implementation, without passing stream and value to test. You can use declval directly inside decltype. Together with comma operator, it looks like this:

#include <type_traits>
#include <utility>
#include <iostream>
#include <sstream>

template<typename S, typename T>
class is_streamable
{
    template<typename SS, typename TT>
    static auto test(int)
    -> decltype( std::declval<SS&>() << std::declval<TT>(), std::true_type() );

    template<typename, typename>
    static auto test(...) -> std::false_type;

public:
    static const bool value = decltype(test<S,T>(0))::value;
};

class C {};

int main() {
    std::cout << is_streamable<std::stringstream, C>::value << std::endl;
    return 0;
}
like image 184
jrok Avatar answered Nov 19 '22 00:11

jrok


jrok's answer causes linkage errors when the value is passed to a function requiring a lvalue (i.e. TheThruth(const bool& t)). So now in C++17 we have template void_t. And based on the example on CPPReference I wrote and tested the following:

#include <iostream>
#include <typeinfo>

template<typename S, typename T, typename = void>
struct is_to_stream_writable: std::false_type {};

template<typename S, typename T>
struct is_to_stream_writable<S, T,
        std::void_t<  decltype( std::declval<S&>()<<std::declval<T>() )  > >
: std::true_type {};


class Foo
{
    public:
    Foo(){}
};

void TheTruth(const bool& t)
{
    std::cout<< t<< std::endl;
}

int main() {
    std::cout<< is_to_stream_writable<std::ostream,int>::value <<std::endl;
    std::cout<< is_to_stream_writable<std::ostream,Foo>::value <<std::endl;
    TheTruth( is_to_stream_writable<std::ostream,int>::value  );

}

Also note the name is_to_stream_writable better fits to operator << and suggests name: is_from_stream_readable for operator >> (better name suggestions are welcome).

The code compiles with g++ -std=c++1z -O0 -Wall -pedantic main.cpp, gcc versions 6.2 and 7.2 and on Coliru.

like image 9
Mijs Tonen Avatar answered Nov 19 '22 00:11

Mijs Tonen