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

6

39

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?

Dextrosinistral answered 31/3, 2014 at 9:39 Comment(0)
A
43

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;
}
Apophyllite answered 31/3, 2014 at 10:44 Comment(5)
@MatthieuM. You're not alone!Apophyllite
Nice catch, i was unable to find the appropriate overload that was catch with rvalue for my answer.Mccammon
Thanks! but does this mean that, probably, this could be reported as a defect of the standard? Maybe the right way should be to enable that overload only if stream << obj is well formed.Dextrosinistral
@gigabytes Perhaps. I/O library wasn't made with metaprogramming in mind, I'd say.Apophyllite
I really like this SFINAE style based on decltype and declval. Extremely readable and template friendly. Great jobGapes
B
9

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.

Bethina answered 28/2, 2018 at 10:5 Comment(1)
Interesting note here is that one must not forget to include the correct header before invoking this. That is, is_to_stream_writable<std::stringstream, int>::value will be false if only including <iosfwd> (which makes it compile), but returns true if including <sstream>.Accusative
S
4

I’m not entirely sure what the issue is, but it works if you remove the std::forwards, and I don’t think they’re necessary here anyway:

template<typename SS, typename TT>
static auto test(SS&& s, TT&& t) -> decltype(s << t);

Live example

Sponsor answered 31/3, 2014 at 10:35 Comment(0)
A
4

One simple way...

template <typename T, class = void>
struct is_streamable : std::false_type { };

template <typename T>
struct is_streamable<T, std::void_t<decltype(std::cout << *(T*)0)>>
  : std::true_type { };

Or, "inspired" by ypw's answer (ypw - if you edit yours accordingly - or create a new one to get rid of the downvotes - I'll delete this and upvote yours):

template <typename T>
class is_streamable
{
    template <typename U> // must be template to get SFINAE fall-through...
    static auto test(const U* u) -> decltype(std::cout << *u);

    static auto test(...)        -> std::false_type;

 public:
    enum { value = !std::is_same_v<decltype(test((T*)0)), std::false_type> };
};

Discussion

The main point of this answer is to highlight how pointless all the worry about rvalue/lvalue references, declvar, forwarding etc. is for this problem. Remember we're just doing a compile time assertion that the streaming notation's supported - there's no run-time for run-time efficiency considerations such as types of references to matter, nor is there a need to use declvar to create a stream as though there was none available. This code keeps it simple and I believe it has full utility - evidence to the contrary most welcome.

Arlie answered 31/3, 2014 at 11:16 Comment(3)
Who's to say that sizeof(std::cout) is not equal to sizeof(char)?Romeu
@ildjarn: used an alternative, though the issue's of no practical relevant IMHOArlie
On the contrary, there's a lot of relevance to pointing out non-portable, implementation-specific code for a portable, implementation-agnostic language. ;-]Romeu
M
1

EDIT : As found by @jrok, it exist a generic operator<< for rvalue streams that interact badly.

Something is really wrong here, if you look at the code below tested on coliru, the 2 last lines compile even if it they should not…

std::stringstream ss;
B b;
int v;

std::cout << typeid(decltype(ss>>v )).name() << "\n" ;
std::cout << typeid(decltype(ss<<1 )).name() << "\n" ;
std::cout << typeid(decltype(std::declval<std::stringstream>()>>v )).name() << "\n" ;
std::cout << typeid(decltype(std::declval<std::stringstream>()<<1 )).name() << "\n" ;

//std::cout << typeid(decltype(ss>>b )).name() << "\n" ; // do not compile
//std::cout << typeid(decltype(ss<<b )).name() << "\n" ; // do not compile
std::cout << typeid(decltype(std::declval<std::stringstream>()>>b )).name() << "\n" ; // should not compile but succeed
std::cout << typeid(decltype(std::declval<std::stringstream>()<<b )).name() << "\n" ; // should not compile but succeed
Mccammon answered 31/3, 2014 at 11:18 Comment(0)
N
0

And to test for '>>' operator:

template<typename IOS, typename T> class can_read_from_ios
{
   static_assert (std::is_base_of<std::ios, IOS>::value);

   template<typename ios, typename t> \
   static auto test (int) -> decltype (std::declval<ios&> () >> std::declval<t&> (), std::true_type ());

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

public:

   static const bool value = decltype (test<IOS, T> (0))::value;
};
Nicety answered 19/8, 2020 at 5:58 Comment(2)
Welcome to StackOverflow! Can you provide an explanation about your solution to make it more clear for the community?Scilla
Hello, thank you for your welcome message and sorry for my late reply. I have updated the previous code as an answer.Nicety

© 2022 - 2024 — McMap. All rights reserved.