Search code examples
c++c++11c++14sfinaeenable-if

Why do I have to call operator<< as a method for SFINAE to work with void_t?


I am trying to define a has_ostream_operator<T> SFINAE test for checking whether I can cout a given type. I have it working, but only if in my definition of has_ostream_operator I call operator<< as a method rather than as an infix operator. In other words this works:

decltype(std::declval<std::ostream>().operator<<(std::declval<T>()))>

This does not:

decltype(std::declval<std::ostream>() << std::declval<T>())>

Test case below (can also see at http://coliru.stacked-crooked.com/a/d257d9d6e0f3f6d9). Note that I included a definition of void_t since I'm only on C++14.

#include <iostream>

namespace std {

    template<class...>
    using void_t = void;

}

template<class, class = std::void_t<>>
    struct has_ostream_operator : std::false_type {};

template<class T>
struct has_ostream_operator<
    T,
    std::void_t<
        decltype(
            std::declval<std::ostream>().operator<<(std::declval<T>()))>>
    : std::true_type {};

struct Foo {};

template<class X>
    void print(
        const X& x,
        std::enable_if_t<has_ostream_operator<X>::value>* = 0)
{
    std::cout << x;
}

template<class X>
    void print(
        const X&,
        std::enable_if_t<!has_ostream_operator<X>::value>* = 0)
{
    std::cout << "(no ostream operator<< implementation)";
}

int main()
{
    print(3); // works fine
    print(Foo()); // this errors when using infix operator version
    return 0;
}

Solution

  • I'm assuming your "infix" version used this expression:

    std::declval<std::ostream>() << std::declval<T>()
    

    The reason that matches for Foo is because the first part, declval<ostream>() produces an rvalue of type ostream&&. This matches a non-member operator<<:

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

    That overload simply forwards the call along:

    Calls the appropriate insertion operator, given an rvalue reference to an output stream object (equivalent to os << value).

    You should instead check for that directly. All the overloads take an ostream by lvalue reference, so you should test that too:

    std::declval<std::ostream&>() << std::declval<T>()