Search code examples
c++templatesoperator-overloadingcontainersextraction-operator

How to force operator>>(C<T>) overload to match container?


I would like to have operator>>() overloads for any "base" type and for any container type. This is what I have so far:

typedef uintmax_t my_t;
template <typename T>
std::istringstream &operator>>(std::istringstream &iss, T &v)
{
    static my_t um = 6009;
    v = um++;
    return iss;
}

template <template <class> class C, typename T>
std::istringstream &operator>>(std::istringstream &iss, C<T> &c)
{
    for (typename C<T>::iterator it = c.begin(); it != c.end(); ++it)
        iss >> *it;
    return iss;
}

int main()
{
    std::vector<uint32_t> vi(3);
    std::istringstream iss;
    iss >> vi;
    for (std::vector<uint32_t>::iterator it = vi.begin(); it != vi.end(); ++it)
        std::cout << *it << std::endl;
}

This compiles and runs as expected with GCC but does not even compile on VS2015. The latter matches the >> operator in the iss >> vi; statement with the first, base-type overload, which triggers other compilation errors. How can I write an operator>>() template for non-container types and a template for container types (without having to specialize for each container type) that compiles with GCC and VS2015?


Solution

  • The operator>> overload you wrote is for a template template class (C) with a single template argument (T). However, std::vector is declared as:

    template<
        class T,
        class Allocator = std::allocator<T>
    > class vector;
    

    The second template argument may be defaulted, but it is still there. As such, std::vector<uint32_t> cannot match C<T> - so the only viable overload is the generic function template that you wrote, which won't compile because you can't assign a std::uintmax_t to a vector.

    In order to get your function to accept vector, you need to match the template template declaration - which means, take a second type argument:

    template <template <class, class> class C, typename T1, typename T2>
    std::istringstream &operator>>(std::istringstream &iss, C<T1,T2> &c)
    {
        for (typename C<T1,T2>::iterator it = c.begin(); it != c.end(); ++it)
            iss >> *it;
        return iss;
    }
    

    This is a pretty unsatisfactory solution though. Really, we want to match on anything that is a container, which we can do using SFINAE. Since this is C++03, the simplest thing would be to write a type trait for whether or not some type has a typedef named iterator:

    template <typename T>
    struct is_container {
        typedef char yes;
    
        struct no {
            char _[2];
        };
    
        template <typename U>
        static yes test( typename U::iterator* );
    
        template <typename U>
        static no test(...);
    
        static const bool value = (sizeof(test<T>(0)) == sizeof(yes));
    };
    

    And add our handy enable_if:

    template <bool, typename >
    struct enable_if { };
    
    template <typename T>
    struct enable_if<true, T> { 
        typedef T type;
    };
    

    And stick that on the return type:

    template <typename C>
    typename enable_if<
        is_container<C>::value,
        std::istringstream&
    >::type
    operator>>(std::istringstream &iss, C& c)
    {
        for (typename C::iterator it = c.begin(); it != c.end(); ++it)
            iss >> *it;
        return iss;
    }
    

    You'll have to do the opposite (!is_container<T>::value) for the other overload so that they're not ambiguous.