Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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;
}
like image 453
Joseph Garvin Avatar asked Sep 08 '15 18:09

Joseph Garvin


3 Answers

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>()
like image 152
Barry Avatar answered Nov 14 '22 06:11

Barry


You need

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

std::declval<std::ostream>() is an rvalue; you are hitting the catch-all operator<< overload for rvalue streams.

like image 43
T.C. Avatar answered Nov 14 '22 06:11

T.C.


If you use infix notation, the rvalue stream inserter is found, since declval returns rvalues per se; [ostream.rvalue]:

template <class charT, class traits, class T>
basic_ostream<charT, traits>& operator<<(basic_ostream<charT, traits>&& os, const T& x);

This overload currently accepts all arguments for x. I've submitted LWG #2534, which, if resolved accordingly, will make your initial code work as expected.

A temporary workaround is to make declval return an lvalue reference, i.e. adjust the template argument to one:

std::declval<std::ostream&>() << std::declval<T>()
like image 4
Columbo Avatar answered Nov 14 '22 05:11

Columbo