Search code examples
c++17sfinae

is it possible to check if overloaded operator<< for type or class exists?


I`m trying to check if overloaded operator<< exists at compile time using c++17. Ideally it is supposed to be something like following:

template<typename T>
static void serialize_storage(const void* this_, std::ostream& stream) {
    if constexpr (std::is_invocable<...>::value) {
        stream << (*reinterpret_cast<const T*>(this_));
    } else {
        throw std::runtime_error("Type can not be serialized");
    }
}

It seems tricky to form parameters for is_invocable as operator<< is overloaded as member of std::ostream or just as "standalone" operator. So I tried two different functions first.

An Example:

#include <map>
#include <string>
#include <iostream>

using Mmap = std::map<std::string, std::string>;

std::ostream& operator<<(std::ostream& stream, const Mmap& map) {
    stream << map.size();
    return stream;
}

template<typename T>
typename std::enable_if<std::is_invocable<decltype (operator <<(std::declval<std::ostream&>(), std::declval<const T&>())), std::ostream&, const T&>::value, void>::type
serialize_storage(const void* this_, std::ostream& stream) {
    stream << (*reinterpret_cast<const T*>(this_));
}

template<typename T>
typename std::enable_if<std::is_invocable<decltype (std::declval<std::ostream>().operator <<(std::declval<T>())), std::ostream, T>::value, void>::type
serialize_storage(const void* this_, std::ostream& stream) {
    stream << (*reinterpret_cast<const T*>(this_));
}

int main(int , char* [])
{

    Mmap foo;
    char boo = 'A';

    serialize_storage<Mmap>(&foo, std::cerr);
    serialize_storage<char>(&boo, std::cerr);
    std::cerr << std::endl;

    return 0;
}

But compiler can not substitute both types. It sees candidates but neither std::ostream::operator<<(char) nor overloaded std::ostream& operator<<(std::ostream& stream, const Mmap& map) fit is_invocable condition.


Solution

  • You could add trait (is_streamable) like this:

    #include <type_traits>
    
    // A helper trait to check if the type supports streaming
    template<class T>
    class is_streamable {
    
        // match if streaming is supported
        template<class TT>
        static auto test(int) ->
            decltype( std::declval<std::ostream&>() << std::declval<TT>(), std::true_type() );
    
        // match if streaming is not supported:
        template<class>
        static auto test(...) -> std::false_type;
    
    public:
        // check return value from the matching "test" overload:
        static constexpr bool value = decltype(test<T>(0))::value;
    };
    
    template<class T>
    inline constexpr bool is_streamable_v = is_streamable<T>::value;
    

    You could then use it like this:

    template<class T>
    static void serialize_storage(const void* this_, std::ostream& stream) {
        if constexpr (is_streamable_v<T>) {
            stream << *reinterpret_cast<const T*>(this_);
        } else {
            throw std::runtime_error("Type can not be serialized");
        }
    }
    

    Or if you want overloads, you can use SFINAE to exclude the function from matching unless T is streamable:

    template <class T>
    static std::enable_if_t<is_streamable_v<T>>
    serialize_storage(const void* this_, std::ostream& stream) {
        stream << *reinterpret_cast<const T*>(this_);
    }
    

    Demo